Advanced

Node.js Microservices Advanced Topics and Best Practices

Node.js is at the forefront of modern web applications, enabling the creation of solutions that are both scalable and high-performance. This course is an in-depth journey into Node.js mic...

Node.js version: 21.6.1 (compatible with Node.js 21)


Table of Contents

  1. Course Overview
  2. Implementing Domain Driven Design (DDD) in microservices
  1. Using CQRS (Command Query Responsibility Segregation) and Event Sourcing
  1. Scaling and optimizing your microservices
  1. Security Best Practices for Microservices
  1. Monolith to Microservices Refactoring Techniques

1. Course Overview

Welcome to this course titled Node.js Microservices: Advanced Topics and Best Practices, taught by Rupesh Tiwari, Senior Solutions Architect at Amazon Web Services.

Node.js is at the forefront of modern web applications, enabling the creation of solutions that are both scalable and high-performance. This course is an in-depth journey into Node.js microservices, covering advanced concepts aimed at developers already familiar with the fundamentals of Node.js.

Main topics covered

  • Domain-Driven Design (DDD): deep dive to structure your microservices
  • CQRS and Event Sourcing: separation of order and request responsibilities, and traceability of events
  • Scaling and optimization: strategies to cope with variable loads
  • Security: authentication, authorization and encryption with the latest practices
  • Refactoring: techniques to migrate from a monolithic architecture to microservices

Prerequisites

  • Good command of JavaScript and basic backend concepts
  • Familiarity with Node.js, RESTful APIs and backend development

What you will learn

At the end of this course, you will have a robust understanding of Node.js microservices and be equipped to:

  • Build, secure and scale complex web applications
  • Design, develop and maintain resilient and high performance backend systems

2. Implementing Domain Driven Design (DDD) in microservices

Module duration: 37m 40s

2.1 Course introduction and prerequisites

This course is based on Node.js version 21.6.1, fully compatible with Node.js 21.

You will gain expertise in five key areas:

  1. Implementing Domain-Driven Design (DDD)
  2. The use of CQRS and Event Sourcing
  3. scaling and optimization of your microservices
  4. Mastering best security practices for microservices
  5. Refactoring techniques for monolithic applications in microservices

With these skills, you will be well equipped to design, develop and maintain resilient, high-performance backend systems.


2.2 Introduction to Domain-Driven Design (DDD)

Domain-Driven Design (DDD) is an approach to software development that seeks to match the complexity of software to the complexity of a business domain. Think of DDD as a tailor-made suit for your software — precisely tailored to your business domain.

Just as an expert tailor creates a suit to perfection, DDD aligns the intricacies of the software with those of your specific field.

Eric Evans, in his groundbreaking book Domain-Driven Design: Tackling Complexities in the Heart of Software, introduced this concept. It’s a transformative approach, whether you’re working on a monolithic or microservices architecture.


2.3 Ubiquitous Language

The Ubiquitous Language is a shared and consistent language used by both software developers and business domain experts to ensure mutual understanding of the complexities and integrations of the domain.

Example with BookStoreHub: Imagine you’re building BookStoreHub and your developers and business experts speak different languages — it’s chaos! Ubiquitous Language bridges this gap. It is a shared, consistent language that ensures everyone understands the complexities of the field.

For example, when discussing BookStoreHub, both developers and business experts refer to it as a “shopping cart” — without confusion. This language simplifies communication, making DDD effective. It’s like having a secret code that unites everyone in your project.

Reminder: The Ubiquitous Language is your key to a harmonious and effective DDD journey.


2.4 Monoliths and microservices

DDD applies to both monoliths and microservices.

In the monolithic world (BookStoreHub as monolith): DDD neatly organizes the code base, like the shelves in a library. Each ray represents a genre — science fiction, mystery, etc. — guaranteeing clear boundaries.

In microservices (BookStoreHub in microservices): The DDD is our compass. Each microservice — like order-service, catalog-service — represents a specialized section of the library. The DDD ensures fluid communication, like the departments of a well-orchestrated bookstore.

Whether organized shelves in a monolith or efficient departments in microservices, DDD brings order and clarity to our software.


2.5 Bounded Contexts in microservices

A Bounded Context defines the boundaries of individual microservices, aligning them with specific business capabilities.

Example with BookStoreHub: Think of order processing and book inventory as two separate bounded contexts:

  • Order Processing: focuses only on order creation and execution
  • Book Inventory: deals with cataloging and managing book inventory

By clearly defining these boundaries, we enable independent development and better communication.

Challenges to overcome:

  • Set these boundaries correctly
  • Manage communication between bounded contexts

2.6 Bounded Context in depth

Bounded contexts address several concrete problems in BookStoreHub:

1. Scalability During a peak in orders during a sale, bounded contexts divide the system into order processing and book inventory, allowing each to scale independently.

2. Bug management During a critical bug in the book recommendation feature, bounded contexts isolate features, making bug fixes more efficient.

3. Team autonomy For example, customer reviews can be managed by a dedicated team, leading to faster improvements.

4. Business alignment Aligning microservices to bounded contexts ensures that each service is finely tuned to meet specific business needs.

In our microservices journey, bounded contexts play the role of both heroes and architects — addressing challenges and shaping our success story.


2.7 Context Mapping

In our microservices journey, we encounter a common challenge: the ubiquitous language clash between bounded contexts.

Conflict example:

  • In the bounded context customer reviews, the term “author” designates a reviewer
  • In the bounded context book inventory, it designates the author of the book

Solution: Context Mapping Context mapping is a crucial technique for defining relationships between bounded contexts. In BookStoreHub we use various strategies:

  • Event Storming: collaborative exploration of domain events to discover shared understandings
  • Domain Storytelling: telling stories about our domains to fill gaps in understanding

By organizing a context mapping workshop for our teams, we can resolve these language conflicts and promote clear communication. Context mapping acts as the bridge between bounded contexts, ensuring a harmonious dialogue in our microservices landscape.


2.8 Strategic Domain-Driven Design

Strategic DDD is our compass for aligning microservices with business capabilities.

Why is this crucial? In BookStoreHub:

  • Bounded context customer reviews requires real-time feedback
  • Bounded context book inventory needs precise inventory updates

Strategic DDD bridges this gap by tailoring microservices to specific business needs.

Key principles:

  • Ubiquitous Language: promoting clear communication
  • Context Mapping: identify relationships between bounded contexts

These practices ensure proper integration and collaboration. As you navigate the intricacies of BookStoreHub, you’ll apply these principles to design microservices that are perfectly aligned with your business goals.


2.9 Demo — Define Bounded Contexts in BookStoreHub

This demo shows how to find bounded contexts in our monolithic BookStoreHub project.

Starting context

The initial BookStoreHub system handles a lot of things: business logic, updating inventory, pricing, order processing, book recommendations and reviews. However, everything is too interconnected — changing one small thing can affect unrelated parts.

Solution: Event Storming

The company has decided to adopt DDD through event storming and has identified four key areas as bounded contexts:

Bounded ContextRole
order-processingCreate and process orders
book-inventoryManage and catalog stock
customer-reviewsCustomer Reviews & Ratings
recommendation-systemBook Recommendations

Directory structure in VS Code

bookstore-hub/
├── contexts/           ← dossier vide au départ (rempli via TDD)
│   ├── bookInventory.js
│   ├── customerReviews.js
│   ├── orderProcessing.js
│   └── recommendationSystem.js
├── application/
│   └── ...
└── tests/
    ├── bookInventory.test.js
    ├── customerReviews.test.js
    ├── orderProcessing.test.js
    └── recommendationSystem.test.js

TDD (Test-Driven Development) approach

Test bookInventory: Verifying that inventory functions, such as checkStock and updateStock, are working correctly.

// bookInventory.test.js - exemple conceptuel
const { checkStock, updateStock } = require('./contexts/bookInventory');

test('checkStock retourne le niveau de stock correct', () => {
  expect(checkStock('isbn-8345')).toBe(50);
});

test('updateStock met à jour le stock correctement', () => {
  updateStock('isbn-8345', 45);
  expect(checkStock('isbn-8345')).toBe(45);
});

CustomerReviews test: Validation of the addition of book reviews via the addReview function.

// customerReviews.test.js - exemple conceptuel
const { addReview } = require('./contexts/customerReviews');

test('addReview ajoute un avis correctement', () => {
  const result = addReview('isbn-8345', { rating: 5, comment: 'Excellent!' });
  expect(result).toBeTruthy();
});

Test orderProcessing: Examines successful order creation, insufficient stock, and missing book details via the createOrder function.


2.10 Working with Aggregates and Entities

In the tactical world of microservices DDD, aggregates and entities are fundamental building blocks.


2.11 Aggregates

An aggregate is a collection of related domain objects — like a shelf in our bookstore microservice.

Example: the book Microservices Patterns by Chris Richardson This book with its unique ISBN (ending with 8345) forms an aggregate with its author and publisher.

Aggregate properties:

  1. Data integrity: This grouping maintains data integrity — when we update a detail, the associated information remains consistent.

  2. Transactional consistency: When a customer purchases Microservices Patterns, the transaction includes inventory deduction and payment processing. If the payment fails, the transaction is rolled back, preserving inventory accuracy.

  3. Application-wide consistency: A change in Microservices Patterns inventory level is immediately reflected across all systems — from inventory to online catalog — preventing discrepancies.

Aggregates in BookStoreHub ensure data integrity and consistency, as seen with Microservices Patterns, its author, publisher, and synchronized updates.


2.12 Entities

entities in DDD are core objects with unique identities.

Entity properties:

  1. Unique identity: A book like Microservices Patterns, identified only by its ISBN (ending in 8345). This unique identity sets it apart in our extensive collection.

  2. Independent lifecycle: Microservices Patterns may see its price increase from $30 to $35, a change that does not alter its author or genre. This adaptability is key to responding to market dynamics without revising the entire catalog.

  3. Real world representation: Our book entity is not just data — it reflects the actual book with its title, author and genre, making the digital environment intuitive.

  4. Behavior and data encapsulation: Microservices Patterns manages its own stock label and categorization, keeping our system orderly and efficient.


2.13 Life cycle of Aggregates and Entities in DDD

Aggregate lifecycle

PhaseDescriptionExample
CreationVia a factory or a manufacturerCreation of the “Science Fiction” category
EditCompliance with business rules and consistencyUpdated to include a subgenre
DeleteExplicit or cascadingDeleting a category reclassifies all associated books

Entity life cycle

PhaseDescriptionExample
CreationWithin an aggregate or independentlyAdded Cosmos by Carl Sagan
EditWithin or independently of the aggregateAdjusted the price of Cosmos without impacting the category
DeleteExplicit or cascadingDeleting a book completely erases its imprint

Aggregates handle creation, consistency, and deletion, while entities provide creation and modification flexibility for smoother project development.


2.14 Collaboration between Aggregates and Entities

In DDD, aggregates and entities work harmoniously together.

Scenario: Adding a new book Space Odyssey

  • The aggregate ensures that the book with its author and genre is added consistently across our system, avoiding any confusion in orders or inventory.
  • The entity (Space Odyssey) maintains its own data: price ($25), number of copies (50) — like a mini-manager that maintains its own data.

Coordinated change: When the genre of Space Odyssey changes from “science fiction” to “classic”, this update propagates system-wide, maintaining synchronization between client-side data and internal records.

Aggregates control lifecycles, while entities organize data for system harmony.


2.15 Demo — Identify Aggregates and Entities in BookStoreHub

Initial problem in inventory service

The addBook function tries to do too many things at once:

  • Checks if book category exists
  • Add books directly
  • Checks that stock and prices are positive

This leads to scattered and repeated checks, making the code complex.

Identified issues (DDD violations):

  1. Test addBook violates the DRY principle by overlapping validations with updateBookPrice
  2. Direct manipulation of stock to a negative value leads to inconsistency
  3. Category-related testing reveals direct booklist manipulation, bypassing protocols
  4. Directly changing the price of a book in a category compromises integrity

Refactoring with TDD

Retest InventoryService:

// inventoryService.test.js (version DDD refactorisée)
const InventoryService = require('./ddd/inventoryService');

describe('InventoryService', () => {
  let service;

  beforeEach(() => {
    service = new InventoryService();
  });

  test('Confirme que InventoryService crée correctement avec validation', () => {
    expect(() => service.addBook({
      isbn: '978-0-13-468599-1',
      title: 'Microservices Patterns',
      price: 40,
      stock: 50
    })).not.toThrow();
  });

  test('Empêche le stock négatif via encapsulation', () => {
    expect(() => service.updateStock('isbn-8345', -1))
      .toThrow('Le stock ne peut pas être négatif');
  });

  test('Les règles métier sont respectées dans les aggregates', () => {
    const category = service.createCategory('Science Fiction');
    expect(category.books).toEqual([]);
  });
});

2.16 Value Objects in DDD

Value Objects represent small, self-contained pieces of data that are immutable and have no identity of their own.

In BookStoreHub, they are used to encapsulate domain concepts like price and ISBN.


2.17 Value Objects in depth

In BookStoreHub, the price of a book, if modified directly, can cause:

  • Historical data alteration: changing the cost of a book from $20 to $25 affects orders placed
  • Lack of traceability
  • Side effects

Solution with Value Objects: If price is a value object, we create a new value object (for example, $25) preserving the old value. This ensures order accuracy and system consistency.

// price.js - Value Object
class Price {
  constructor(amount) {
    if (amount < 0) {
      throw new Error('Le prix ne peut pas être négatif');
    }
    this._amount = amount;
    Object.freeze(this); // immutabilité
  }

  get amount() {
    return this._amount;
  }

  applyDiscount(percentage) {
    // Retourne un NOUVEAU value object (immutabilité)
    return new Price(this._amount * (1 - percentage / 100));
  }

  equals(other) {
    return other instanceof Price && other.amount === this._amount;
  }
}

module.exports = Price;
// isbn.js - Value Object
class ISBN {
  constructor(value) {
    if (!this._isValidISBN(value)) {
      throw new Error('Format ISBN invalide');
    }
    this._value = value;
    Object.freeze(this);
  }

  _isValidISBN(value) {
    // Validation du format ISBN-13
    return /^978-\d{1,5}-\d{1,7}-\d{1,7}-\d$/.test(value);
  }

  get value() {
    return this._value;
  }

  equals(other) {
    return other instanceof ISBN && other.value === this._value;
  }
}

module.exports = ISBN;

2.18 The importance of immutability in DDD

Immutability is fundamental in DDD for several reasons:

1. Consistency for customers

Imagine a customer choosing Cosmos by Carl Sagan for $20. By treating price as a value object, you ensure solid price consistency during their shopping session. Result: fewer price discrepancies, more satisfied customers, increased trust.

2. Prevention of competition problems

In a crowded bookstore with customers rushing to get the latest copy of Space Odyssey, if prices can change on the fly, two people might see different prices for the same book. Immutability, where Space Odyssey is set at $20, prevents this chaos — everyone pays the same price.

3. Consistency in distributed microservices

Consider Microservices Patterns initially at $40. Immutability ensures that this price remains constant. If a business update requires a change to $45, you create a new immutable Price value object, preserving the integrity of old orders with the old price while applying the new price for future orders.

4. Simplified testability

Without immutability, problems arise when testing in a distributed microservices environment: inconsistent test results, complex debugging, resource-intensive testing, and complex test case management. With price immutability, we benefit from stable and efficient tests with reliable results.


2.19 Demo — Identify Value Objects in BookStoreHub

Identified issues

book.js and recommendation.js files (before refactoring): Both files validate ISBN and price directly in the manufacturers — a clear violation of DDD principles.

Helper file: The ISBN validation helper function risks a “leaky” abstraction, detached from domain logic.

Refactoring: introduction of Value Objects

Update book.js:

// book.js (après refactorisation avec Value Objects)
const ISBN = require('./isbn');
const Price = require('./price');

class Book {
  constructor({ isbn, title, author, price, stock }) {
    this._isbn = new ISBN(isbn);        // Value Object
    this._price = new Price(price);     // Value Object
    this._title = title;
    this._author = author;
    this._stock = stock;
  }

  get isbn() { return this._isbn.value; }
  get price() { return this._price.amount; }
  get title() { return this._title; }

  updatePrice(newAmount) {
    // Crée un nouveau Value Object - préserve l'immuabilité
    this._price = new Price(newAmount);
  }
}

module.exports = Book;

Update recommendation.js:

// recommendation.js (après refactorisation)
const ISBN = require('./isbn');

class Recommendation {
  constructor({ isbn, bookTitle, score }) {
    this._isbn = new ISBN(isbn);  // Value Object réutilisé
    this._bookTitle = bookTitle;
    this._score = score;
  }

  get isbn() { return this._isbn.value; }
}

module.exports = Recommendation;

2.20 DDD and Processes — A Symbiotic Relationship

Now that we’ve explored DDD and its key concepts, let’s look at how DDD and your processes can create a symbiotic relationship — how they can work together to improve your software development practices.


2.21 DDD in Agile / Scrum

How can DDD boost your Agile or Scrum processes in developing microservices like BookStoreHub?

1. DDD improves development processes

By integrating DDD, we streamline decision-making. Imagine the task of building a recommendation system:

  • DDD breaks this big task into smaller, manageable pieces
  • Sprint 1: focus on user preferences — this lays solid foundations
  • Sprint 2: integration of book data

Result: notable progress every two weeks, steady pace and clear direction throughout the project.

2. DDD promotes collaboration and communication

Using DDD, everyone — from developers to business analysts — speaks the same language.

Example: the new “Seasonal Reading List” functionality: The marketing team comes up with special book lists for the summer. With the DDD:

  • You define a book list for the book collection
  • season for time of year This clarifies concepts for everyone, turning a great idea into a popular feature in time for summer.

3. DDD improves software design

Consider the challenge of updating book categories. With the DDD:

  • A rigid design is replaced by a flexible category system
  • Easy addition of new genres like “climate fiction”
  • Adapting to market trends without embarking on a major project

4. DDD improves stability and maintainability

Imagine integrating a new payment gateway. Without DDD, every minor adjustment affects other services. With the DDD:

  • Payment functionality is segregated as a standalone component
  • Independent updates and tests
  • Significantly reduced system downtime – Improved performance, resilience and flexibility

Combining DDD with Agile simplifies complexity, promotes teamwork, increases flexibility and provides stability. It’s like pairing a skillful captain with a detailed map for a successful developmental journey.


3. Use of CQRS and Event Sourcing

Module duration: 28m 2s

3.1 Introduction to CQRS module

Understanding CQRS (Command Query Responsibility Segregation) is critical because it precisely targets and resolves the complexities of managing data access and state mutations, particularly under high demand.

For BookStoreHub, this isn’t just an improvement — it’s a necessary evolution to maintain high performance and scalability in the face of a growing user and data base.


3.2 Understanding CQRS

CQRS is based on three core components:

1. The Command Model

This is where we define how the data is modified. Just like a new book arriving in BookStoreHub needs a place on a shelf, every update, deletion, or new entry is a command that reshapes our database.

2. The Query Model

Optimized to make data recovery fast. Think of it like the quick search feature on BookStoreHub that helps you find your favorite author or genre in an instant.

3. Event Handling

Like BookStoreHub historical log. It records every small change, so we can understand customer habits and stock movements over time.

┌─────────────────────────────────────────────────────────┐
│                    Architecture CQRS                     │
├──────────────────────┬──────────────────────────────────┤
│   COMMANDES (Write)  │      REQUÊTES (Read)              │
├──────────────────────┼──────────────────────────────────┤
│ - Créer commande     │ - Récupérer catalogue             │
│ - Mettre à jour stock│ - Rechercher livres               │
│ - Poster un avis     │ - Obtenir détails livre           │
│ - Traiter paiement   │ - Lister recommandations          │
└──────────────────────┴──────────────────────────────────┘
          │                          │
          ▼                          ▼
    PostgreSQL                    MongoDB/Redis
    (writes sécurisés)           (reads optimisés)

Keep your commands accurate, your queries fast, and your events logged. We are thus creating a robust and user-friendly BookStoreHub, ready to serve thousands of book lovers every day.


3.3 Event Sourcing in microservices

Since our CQRS founding, we have been venturing into Event Sourcing within our microservices — meticulously recording every change in BookStoreHub as a sequence of events.

Imagine a librarian’s diary tracking each book borrowed. Likewise, our event log captures every transaction.

Key concepts

1. Event logging When a customer purchases a book or posts a review in BookStoreHub, it’s logged — creating a story for smooth tracking and auditing.

2. State Reconstruction What if we need to rewind the narrative to examine the journey of a bestseller? Enter State Reconstruction, our time machine for data — reconstructing past scenarios from events for insights into customer behavior and inventory levels.

3. Event Processing Not just recording events but orchestrating them in a symphony. Imagine the frenzy of a flash sale. Event processing ensures that every fast transaction is recorded flawlessly.

By integrating Event Sourcing as part of BookStoreHub, we create microservices that are not only robust, but deeply insightful. Journal with precision, reconstruct with clarity, and process events with acuity.


3.4 Read Models — Create projections with CQRS

Projections transform event streams into models that our microservices can query. This makes data access quick and personalizes the data view for each user.

1. Event-to-Model

In BookStoreHub, imagine distilling a data stream of complex events — like a multitude of book purchases — into a structured model that gives us a clear picture of current inventory levels.

2. Read Optimization

Fine-tuning these models for faster data access. If BookStoreHub’s query model is slow, a customer could experience delays in finding their desired book, affecting the user experience. Hence the optimization to guarantee rapid and efficient access.

3. State Alignment

Crucial for keeping our read models in sync with the latest events. For example, when a new book title is added to BookStoreHub’s catalog, the read model immediately updates to reflect that edition — ensuring that customers see the information in real time.


3.5 Checklist of projections and their essence

Before implementing projections, check:

CriterionDescription
Sync with domain eventsKeep projections up to date
Regular checkCheck and optimize the structures of read models
Capacity AssessmentWill the projections support your data load and query speed?

Projections vs Event Sourcing: Projections focus not on the historical sequence, but on creating a snapshot of the present day for quick and easy readings. It’s the difference between having every transaction recorded and a current bank balance.


3.6 Messaging integration in microservices

In the integrated messaging with DDD in BookStoreHub:

1. Domain Events

Domain events are the heart of the reactions of our microservices. When a new book is added to the inventory, a book.added event is broadcast. This action, mapped directly in our DDD, informs other services of the new edition without direct coupling between them.

2. Messaging channels

Messaging channels act as the arteries of our system, carrying vital information to every department that needs it. Once the book.added event is raised, it propagates through channels that ensure the recommender system knows there is a new player.

3. Reactive systems

While similar to message handlers, they play a slightly different tune. Responsive systems maintain a real-time record of changes. When a book.added event occurs, our read models are updated on the fly.

Recommended libraries and services:

// Options de messagerie pour Node.js
const messagingOptions = {
  // Synchrone et scalable
  rabbitmq: 'amqplib',          // npm install amqplib
  
  // Temps réel
  socketIO: 'socket.io',        // npm install socket.io
  
  // Cloud-native
  awsSQS: '@aws-sdk/client-sqs', // npm install @aws-sdk/client-sqs
  gcpPubSub: '@google-cloud/pubsub' // npm install @google-cloud/pubsub
};

3.7 CQRS in action — The Book of the Month feature

Business context

BookStoreHub launches Book of the Month initiative. With the projected increase in traffic (70% increase, a peak of 2000 requests per second), the goal is to maintain response times below 2 seconds.

Triple technical challenge

1. Performance efficiency The current architecture is vulnerable to database lock contention and transaction bottlenecks during periods of high volume — potentially causing slowdown that impacts user experience.

2. Scalability Existing infrastructure is not designed for independent scaling of read and write operations, essential to handle the varied loads expected with the release of the new feature.

3. System resilience In case of high concurrency, we risk potential system crashes.


3.8 Architecture Assessment Checklist

Before diving into the code for the Book of the Month functionality:

CriterionObjectiveStatus
ScalabilityManage 2000 RPS for reads, queue for writes✅ Smart scaling
PerformanceResponse time < 2 seconds during peak✅ Mandatory
ResilienceWithstand peak traffic without failure✅ Bullet-proof architecture
MaintainabilityClean and manageable codebase✅ CQRS separates commands and queries
DecouplingServices operating independently✅ CQRS facilitates smooth interaction
Data consistencyUsers see the most recent data✅ Event-driven nature of CQRS

3.9 CQRS implementation for Book of the Month functionality

Here are the 13 steps of CQRS implementation for Book of the Month:

StepDescription
1Book of the Month announcement (trigger)
2User activity surge
3Increased read operations
4Increased write operations
5Scaling read models for speed and efficiency
6Separate queue for write operations
7MongoDB for fast reads
8PostgreSQL for secure writes
9Real-time data synchronization
10Scaling read and write services independently
11Read service scaling
12Scaling of the write service
13Maintaining responsiveness and reliability

3.10 Demo — Optimizing reads with Redis and CQRS

Prerequisites

  • Redis installed and running locally
  • Installation guide available in course materials and on GitHub

Context

Business Challenge: Effectively manage a 70% increase in user traffic. To address this, we employ the separation of CQRS read and write operations.

Structure of the read-service directory

read-service/
├── config/
│   └── redisConfig.js      ← Configuration connexion Redis
├── controllers/
│   └── bookController.js   ← Gestion des requêtes entrantes (pattern CQRS)
├── services/
│   └── bookService.js      ← Logique de récupération des données
├── routes/
│   └── bookRoutes.js       ← Définition des routes
└── index.js                ← Point d'entrée, lancement du serveur

Redis Configuration

// config/redisConfig.js
const redis = require('redis');

const redisClient = redis.createClient({
  host: process.env.REDIS_HOST || 'localhost',
  port: process.env.REDIS_PORT || 6379,
  retry_strategy: (options) => {
    if (options.error && options.error.code === 'ECONNREFUSED') {
      return new Error('Le serveur Redis a refusé la connexion');
    }
    if (options.total_retry_time > 1000 * 60 * 60) {
      return new Error('Dépassement du temps de reconnexion');
    }
    return Math.min(options.attempt * 100, 3000);
  }
});

redisClient.on('connect', () => console.log('✅ Connecté à Redis'));
redisClient.on('error', (err) => console.error('❌ Erreur Redis:', err));

module.exports = redisClient;

Controller with Redis cache (CQRS pattern)

// controllers/bookController.js
const redisClient = require('../config/redisConfig');
const bookService = require('../services/bookService');

const getBookOfTheMonth = async (req, res) => {
  const cacheKey = 'book:of:the:month';
  
  try {
    // 1. Vérifier le cache Redis en premier
    const cachedData = await redisClient.get(cacheKey);
    
    if (cachedData) {
      console.log('📦 Données servies depuis le cache Redis');
      return res.json(JSON.parse(cachedData));
    }
    
    // 2. Si pas en cache, aller chercher les données
    const book = await bookService.getBookOfTheMonth();
    
    // 3. Stocker en cache pour 1 heure
    await redisClient.setEx(cacheKey, 3600, JSON.stringify(book));
    
    console.log('🗄️ Données récupérées depuis la base de données');
    res.json(book);
    
  } catch (error) {
    console.error('Erreur lors de la récupération du livre du mois:', error);
    res.status(500).json({ error: 'Erreur interne du serveur' });
  }
};

module.exports = { getBookOfTheMonth };

Note: Redis serves our current needs, but you are encouraged to explore other caching options like Memcached or Aerospike to find the best fit for your project.

What this demo covers

  • Steps 3 and 5 of the architecture diagram: read operations and scaling independent of read models
  • Step 7: read models optimized via Redis

3.11 Demo — Resilient writes with RabbitMQ

Prerequisites

  • RabbitMQ installed and running

Role of RabbitMQ

RabbitMQ is a message broker that allows applications to communicate asynchronously. It is used here for its ability to queue messages, providing a buffer and ensuring that write operations are processed smoothly, even when our system experiences traffic spikes.

Structure of the write-service directory

write-service/
├── config/
│   └── amqpConfig.js       ← Configuration connexion RabbitMQ
├── models/
│   └── bookModel.js        ← Interactions avec la base de données
├── queues/
│   └── bookQueue.js        ← Gestion de la consommation des messages
├── scripts/
│   ├── sendAddReviewMessage.js      ← Script test: ajouter un avis
│   └── sendUpdateInventoryMessage.js ← Script test: MAJ inventaire
└── index.js

RabbitMQ Configuration

// config/amqpConfig.js
const amqp = require('amqplib');

const RABBITMQ_URL = process.env.RABBITMQ_URL || 'amqp://localhost';
const BOOK_QUEUE = 'book_operations';

async function connectRabbitMQ() {
  try {
    const connection = await amqp.connect(RABBITMQ_URL);
    const channel = await connection.createChannel();
    
    // Créer la file d'attente durable (survit aux redémarrages)
    await channel.assertQueue(BOOK_QUEUE, { durable: true });
    
    console.log('✅ Connecté à RabbitMQ');
    return { connection, channel };
  } catch (error) {
    console.error('❌ Erreur connexion RabbitMQ:', error);
    throw error;
  }
}

module.exports = { connectRabbitMQ, BOOK_QUEUE };

Queue management

// queues/bookQueue.js
const { connectRabbitMQ, BOOK_QUEUE } = require('../config/amqpConfig');

async function consumeBookOperations() {
  const { channel } = await connectRabbitMQ();
  
  // Ne pas envoyer plus d'un message à la fois (fair dispatch)
  channel.prefetch(1);
  
  console.log(`⏳ En attente de messages dans la file "${BOOK_QUEUE}"...`);
  
  channel.consume(BOOK_QUEUE, async (message) => {
    if (message !== null) {
      const operation = JSON.parse(message.content.toString());
      console.log('📨 Opération reçue:', operation);
      
      try {
        await processBookOperation(operation);
        channel.ack(message); // Accusé de réception
      } catch (error) {
        console.error('Erreur traitement:', error);
        channel.nack(message, false, true); // Remise en file d'attente
      }
    }
  });
}

async function processBookOperation(operation) {
  // Traiter selon le type d'opération
  switch (operation.type) {
    case 'ADD_REVIEW':
      // Logique d'ajout d'avis
      break;
    case 'UPDATE_INVENTORY':
      // Logique de mise à jour d'inventaire
      break;
    default:
      throw new Error(`Type d'opération inconnu: ${operation.type}`);
  }
}

module.exports = { consumeBookOperations };

Note: Command handlers are important in CQRS, but have been simplified for the purposes of the demo. In a complete system, you would see commands and command handlers to process business logic in a more distinct way.


3.12 System Architecture Overview

This architecture illustrates how Redis, RabbitMQ and Kafka work together in our BookStoreHub system:

                         BookStoreHub - Architecture CQRS + Event Sourcing
                         
    ┌─────────┐    READ (requêtes rapides)    ┌─────────────┐    ┌───────┐
    │  User   │ ───────────────────────────▶ │ Read Service│───▶│ Redis │
    │(client) │                              │             │    │(cache)│
    │         │    WRITE (commandes)          └─────────────┘    └───────┘
    │         │ ───────────────────────────▶ ┌──────────────┐
    └─────────┘                             │ Write Service│
                                            └──────┬───────┘
                                                   │
                             ┌─────────────────────┼──────────────────────┐
                             │                     │                      │
                             ▼                     ▼                      ▼
                       ┌──────────┐         ┌──────────┐          ┌──────────┐
                       │RabbitMQ  │         │  Kafka   │          │ MongoDB  │
                       │(queuing) │         │(events)  │          │(write DB)│
                       └──────────┘         └──────────┘          └──────────┘

Component roles:

ComponentRole
RedisIn-memory data store — ultra-fast data retrieval for Read Service
Read ServiceFast data delivery without delay
Write ServiceCaptures user inputs accurately, respecting the exact order of transactions
RabbitMQOrderly queue — handles high-throughput loads, preserves order of operations, ensures reliable message delivery
KafkaRobust archivist — logs events with durability and fault tolerance, partitioning for immutability of event sequences

RabbitMQ and Kafka together form the backbone of the system — handling real-time operations and long-term event retention.


3.13 Demo — Event Sourcing in practice

Prerequisites

  • Kafka 3.7 installed and running
  • Zookeeper active (required by Kafka)

Technical Challenge

Ensure the system can handle a 70% increase in traffic, with a peak of 2000 req/s, while maintaining a load time of less than 2 seconds.

Why Kafka?

Kafka is a distributed event streaming platform designed to handle high-throughput data streams. Kafka is ideal because it provides the durability and reliability our system needs to remain resilient under load.

Structure of the event-sourcing-service directory

event-sourcing-service/
├── kafkaConfig.js            ← Configuration du client Kafka
├── ReadModelUpdater.js       ← Mise à jour des read models en mémoire
├── BookEventPublisher.js     ← Publication des événements via Kafka
├── BookEventStore.js         ← Stockage temporaire des événements
└── index.js                  ← Point d'entrée, démarrage du moteur d'event sourcing

Kafka configuration

// kafkaConfig.js
const { Kafka } = require('kafkajs');

const kafka = new Kafka({
  clientId: 'bookstorehub-event-sourcing',
  brokers: [process.env.KAFKA_BROKER || 'localhost:9092'],
  retry: {
    initialRetryTime: 100,
    retries: 8
  }
});

module.exports = kafka;

Book Event Editor

// BookEventPublisher.js
const kafka = require('./kafkaConfig');

const producer = kafka.producer();
const BOOK_EVENTS_TOPIC = 'book-events';

async function publishBookEvent(eventType, eventData) {
  await producer.connect();
  
  const event = {
    eventId: Date.now().toString(),
    eventType,        // ex: 'BOOK_ADDED', 'REVIEW_POSTED', 'STOCK_UPDATED'
    timestamp: new Date().toISOString(),
    data: eventData
  };
  
  await producer.send({
    topic: BOOK_EVENTS_TOPIC,
    messages: [
      {
        key: eventData.bookId,
        value: JSON.stringify(event)
      }
    ]
  });
  
  console.log(`📤 Événement publié: ${eventType}`);
  await producer.disconnect();
}

module.exports = { publishBookEvent };

Event storage

// BookEventStore.js
class BookEventStore {
  constructor() {
    // En production: persistance en base de données
    this._events = [];
  }
  
  async appendEvent(event) {
    this._events.push({
      ...event,
      sequenceNumber: this._events.length + 1
    });
  }
  
  async getEventsForBook(bookId) {
    return this._events.filter(e => e.data.bookId === bookId);
  }
  
  async reconstructState(bookId) {
    const events = await this.getEventsForBook(bookId);
    return events.reduce((state, event) => {
      return applyEvent(state, event);
    }, {});
  }
}

module.exports = new BookEventStore();

Starting the demo

# Terminal 1: Démarrer Zookeeper
bin/zookeeper-server-start.sh config/zookeeper.properties

# Terminal 2: Démarrer Kafka
bin/kafka-server-start.sh config/server.properties

# Terminal 3: Démarrer le service d'event sourcing
cd event-sourcing-service
npm install
node index.js

4. Scaling and optimizing your microservices

Module duration: 34m 23s

4.1 Introduction to the Scaling module

In this module, you will learn critical scaling strategies to handle heavy loads and maintain performance. Topics covered include:

  • load balancing
  • Database optimization
  • service discovery

The goal: to ensure that your services can grow and adapt to user demand efficiently.


4.2 Introduction to microservices scaling

Scaling is the ability of our system to remain efficient and strong no matter how user demand changes. Think of our system as a living thing — scaling is its natural response to adapt to its environment.

Why scale?

  • Meeting SLAs (Service Level Agreements) — our promise to deliver consistent performance
  • Manage traffic spikes related to marketing campaigns, new launches or organic growth
  • Maintaining the balance between service demand and delivery

Scaling types

Horizontal scaling (Scale Out / Scale In) Like adding more lines at a supermarket to handle increased traffic. We deploy our application on more servers, which can be automated in the cloud, balancing the load.

Avant scaling:           Après scaling horizontal:
┌───────────┐            ┌───────────┐ ┌───────────┐ ┌───────────┐
│  Serveur  │            │ Serveur 1 │ │ Serveur 2 │ │ Serveur 3 │
│(surcharge)│            │           │ │           │ │           │
└───────────┘            └───────────┘ └───────────┘ └───────────┘

Vertical scaling (Scale Up / Scale Down) Upgrade existing server capacity by adding CPUs, memory or storage. Like upgrading a car engine for more power. Quick to execute, but limited.

For monolithic BookStoreHub, horizontal scaling optimizes costs by scaling for traffic surges and scaling down after peak hours. Vertical scaling, while sometimes necessary, is not preferred in a future microservices architecture — reserved for critical, compute-intensive components.


4.3 Mastering load balancing in microservices

load balancing is the technology that keeps our microservices well distributed and high performance under varying loads.

Goal: Distribute traffic intelligently across servers to prevent a single point from being overwhelmed — like a well-trained team where work is shared fairly.

Types of load balancers

TypeFeatures
SoftwareVery adaptable, integrates with the cloud, solutions: NGINX, HAProxy, Traefik
HardwareRaw power, for traditional on-premises data centers: F5 BIG-IP, Citrix

Distribution algorithms

AlgorithmDescriptionUse cases
Round RobinFair rotation, passes each request to the next serverUniform load
Least ConnectionsPrioritize servers with fewer active connectionsLong-running queries
IP HashDirects a user to the same server based on their IPUser Sessions

4.4 Compare software and hardware load balancers

Software Load Balancers

  • Benefits: dynamic scaling, agile, expanding and contracting with traffic flow, reduced operational costs, cloud compatibility
  • Solutions: NGINX, HAProxy, Traefik
  • Ideal for: cloud-centric environments, transition to scalable environments

Hardware Load Balancers

  • Benefits: Robust performance, ideal for environments requiring physical control, handling massive traffic volumes
  • Solutions: F5 BIG-IP, Citrix
  • Ideal for: on-premises deployments where reliable control is essential

For monolithic BookStoreHub:

  • A software load balancer is suitable for a transition to a cloud-focused and scalable environment
  • A hardware load balancer efficiently handles high and constant traffic on-premises, enabling future architectural planning

4.5 Cloud Load Balancers

Cloud-native options are tailor-made for cloud environments:

SupplierServicesFeatures
AWSAmazon ELB (Elastic Load Balancing)Integrates seamlessly with AWS services, resilient and auto-scaling solution
Microsoft AzureAzure Load BalancerAzure native choice, high availability and security in the Azure ecosystem
Google CloudGoogle Cloud Load BalancerGlobal traffic management across Google’s vast network

These Cloud Native solutions simplify load balancing in the cloud with their managed services and their deep integration into their respective cloud platforms.


4.6 Ensuring system health in microservices

Imagine a doctor regularly checking vital signs. This is what automated server health checking is.

Health check methods

MethodDescriptionAnalogy
HTTPQuick pulse checkRapid pulse test
TCPReflex testReflex test
Custom scriptsComplete health checkupComplete medical examination

Configuring health checks

# nginx.conf - exemple de configuration des health checks
upstream bookstore_backend {
    server localhost:3001;
    server localhost:3002;
    server localhost:3003;
    
    # Health check passif
    # Active health check nécessite NGINX Plus
}

server {
    listen 80;
    server_name bookstorehub.com;
    
    location /health {
        access_log off;
        return 200 'healthy\n';
        add_header Content-Type text/plain;
    }
    
    location / {
        proxy_pass http://bookstore_backend;
        proxy_connect_timeout 5s;
        proxy_read_timeout 30s;
    }
}

Health check tools:

  • Kubernetes: liveness probes and readiness probes
  • AWS: ELB health checks
  • Consul: integrated health checks service

Health checks integrate with load balancing to form a safety net — any server not capable of traffic is bypassed, maintaining a smooth user experience.


4.7 Demo — Adding a load balancer to BookStoreHub

Context

BookStoreHub recently experienced a 2 hour outage during a high traffic sales event. Result:

  • Loss of more than $20,000
  • Noticeable drop in customer satisfaction
  • Bottleneck: Single server that could not handle the peak
  • Response time exceeding 5 seconds (well beyond acceptable threshold)

Solution: NGINX as software load balancer

Prerequisites:

  • NGINX pre-installed on your system

Directory structure

bookstore-load-balanced/
├── catalog/               ← Microservice Node.js avec base de données en mémoire
│   ├── app.js
│   └── package.json
├── load-test/
│   └── artillery.yml      ← Configuration load test (10 nouveaux users/sec, 1 min)
└── nginx/
    ├── nginx.conf         ← Configuration principale NGINX
    └── sites-available/
        └── bookstorehub   ← Configuration de production BookStoreHub

NGINX Configuration

# nginx/sites-available/bookstorehub
upstream catalog_service {
    # Consolidation des configurations serveurs backend en une seule ligne
    server localhost:3001;
    server localhost:3002;
    server localhost:3003;
    
    # Algorithme: Round Robin par défaut
    # Pour Least Connections: least_conn;
    # Pour IP Hash: ip_hash;
}

server {
    listen 80;
    server_name bookstorehub.local;
    
    location / {
        proxy_pass http://catalog_service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Artillery load test configuration

# load-test/artillery.yml
config:
  target: 'http://bookstorehub.local'
  phases:
    - duration: 60          # 1 minute
      arrivalRate: 10        # 10 nouveaux utilisateurs par seconde
      # Total: 600 requêtes
  
scenarios:
  - name: 'Test catalogue de livres'
    requests:
      - get:
          url: '/catalog/books'
# Exécuter le load test
npm install -g artillery
artillery run load-test/artillery.yml

4.8 Service Discovery — The microservices compass

Service discovery is crucial for orchestrating microservice interactions — a best practice that brings dynamism and resilience to cloud environments like AWS.

Comparison: Service Discovery vs Static Approaches

ApproachAdvantagesDisadvantages
Discovery ServiceReal-time update, adaptive, dynamicMore complex to configure
Blue/Green DeploymentEffective for transitionsDoes not maintain a continuous record of services
A/B DeploymentUseful for testingSame
Local DLLs and JARsSimpleFixed routes — problems if services change

Operation

How does one service find another in BookStoreHub’s maze of microservices? Service discovery serves as a definitive guide, cataloging services for accelerated discovery.

It is a lifesaver in a microservices architecture, avoiding the pitfalls of tightly coupled local dependencies.


4.9 Service Registry Mechanics

The service registry is the essential compass in the world of microservices.

How it works in practice

1. Self-registration When deploying a new shipping service in BookStoreHub, as soon as it is online, it self-registers in the service registry. This isn’t just a best practice — it’s a fundamental principle that keeps the ecosystem agile.

2. Service Lookup Our registry is a live directory constantly queried by customers for the most up-to-date service endpoints. Whether you use Consul, Eureka, or Kubernetes services in a Node.js application — all catalog services for fast and reliable lookups.

3. Health Checks Like regular health checkups. For example, a payment processing service may be running but unable to connect to a banking API. Without constant health checks, we are blindly sending requests to a failing service. With Kubernetes liveness and readiness probes, we keep a vigilant eye on the integrity of our network.

4. Decentralized discovery What to do if the registry itself goes down? Decentralized discovery mitigates this risk. Services use strategies like gossip protocols or DNS for peer-to-peer discovery. Libraries like Kote make it possible to build such resilient service networks.


4.10 Demo — Implement dynamic Service Discovery in BookStoreHub

Consul by HashiCorp

Consul is an open-source service mesh solution provided by HashiCorp, offering:

  • Service discovery
  • Health checking
  • Key-value storage
  • And more

Environment Configuration

# Démarrer Consul en mode développement
consul agent -dev

# Interface UI disponible à:
# http://localhost:8500/ui

Project structure

bookstorehub-consul/
├── order-service/
│   ├── app.js          ← Logique métier (Express.js)
│   ├── consul.js       ← Enregistrement et découverte Consul
│   └── package.json
└── payment-service/
    ├── app.js          ← Endpoint de traitement des paiements
    ├── consul.js       ← Enregistrement et découverte Consul
    └── package.json

Registration in Consul

// consul.js (partagé par les deux services)
const Consul = require('consul');

const consul = new Consul({
  host: process.env.CONSUL_HOST || 'localhost',
  port: process.env.CONSUL_PORT || 8500
});

async function registerService(serviceName, servicePort) {
  const serviceId = `${serviceName}-${process.pid}`;
  
  await consul.agent.service.register({
    id: serviceId,
    name: serviceName,
    address: 'localhost',
    port: servicePort,
    check: {
      http: `http://localhost:${servicePort}/health`,
      interval: '10s',      // Health check toutes les 10s
      timeout: '5s',
      deregistercriticalserviceafter: '30s'
    },
    tags: ['bookstore', serviceName]
  });
  
  console.log(`✅ Service "${serviceName}" enregistré dans Consul (ID: ${serviceId})`);
  return serviceId;
}

async function discoverService(serviceName) {
  const result = await consul.health.service({
    service: serviceName,
    passing: true  // Seulement les services sains
  });
  
  if (result[0].length === 0) {
    throw new Error(`Service "${serviceName}" introuvable dans Consul`);
  }
  
  const { Service } = result[0][0];
  return `http://${Service.Address}:${Service.Port}`;
}

module.exports = { registerService, discoverService };

Payment service

// payment-service/app.js
const express = require('express');
const { registerService } = require('./consul');

const app = express();
const PORT = 3002;

app.use(express.json());

// Endpoint de traitement des paiements
app.post('/process', (req, res) => {
  const { orderId, amount } = req.body;
  
  // Simulation du traitement du paiement
  res.json({
    success: true,
    transactionId: `TXN-${Date.now()}`,
    orderId,
    amount,
    timestamp: new Date().toISOString()
  });
});

app.get('/health', (req, res) => res.json({ status: 'healthy' }));

app.listen(PORT, async () => {
  console.log(`Payment Service démarré sur le port ${PORT}`);
  await registerService('payment-service', PORT);
});

Order service using service discovery

// order-service/app.js
const express = require('express');
const axios = require('axios');
const { registerService, discoverService } = require('./consul');

const app = express();
const PORT = 3001;

app.use(express.json());

app.post('/orders', async (req, res) => {
  try {
    // Découvrir le payment-service via Consul
    const paymentServiceUrl = await discoverService('payment-service');
    
    // Appeler le payment-service découvert dynamiquement
    const paymentResponse = await axios.post(`${paymentServiceUrl}/process`, {
      orderId: req.body.orderId,
      amount: req.body.total
    });
    
    res.json({
      order: req.body,
      payment: paymentResponse.data,
      status: 'confirmed'
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(PORT, async () => {
  console.log(`Order Service démarré sur le port ${PORT}`);
  await registerService('order-service', PORT);
});

4.11 Database Sharding

After scaling our application with the discovery service, let’s approach the data itself with database sharding.

Sharding involves dividing a large library into several smaller, specialized sections, each hosted on a different server for better access and management.

Problem without sharding

Imagine BookStoreHub during a flash sale. A single database, like a crowded checkout line, leads to delays and frustrated users.

Solution: sharding

Data distribution acts like express files:

  • Partitioning by genre or author
  • Prevention of traffic jams (hot spots)
  • Transaction Acceleration

Choose a shard key

Choosing a shard key is like mapping the fastest routes in a city. We could choose:

  • User ID: fair distribution of user load
  • Geolocation: deliver data faster to users based on their region
  • ISBN range: distribution by genre or letter
// Exemple conceptuel de routing de shards
function getShardForBook(isbn) {
  const shards = ['shard-1', 'shard-2', 'shard-3'];
  // Algorithme de hashing cohérent
  const hashValue = hashCode(isbn) % shards.length;
  return shards[hashValue];
}

function hashCode(str) {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash = hash & hash; // Convert to 32bit integer
  }
  return Math.abs(hash);
}

Database sharding is not just a matter of slicing data. It’s a strategic decision that combats latency, encourages equitable load distribution, and prepares BookStoreHub for a thriving future.


4.12 Caching techniques

Caching is our secret weapon for fast data recovery — like keeping your most-used books on the bedside table for easy access.

Key Benefits

  1. Dramatically reduced response times: the most requested data is at your fingertips
  2. Smart cache policy: like a librarian who knows exactly when to replace old editions with new ones — cache validity management
  3. Reducing database load: During peak hours, caching is like an expert crowd manager, easing database load and conserving resources
  4. Intelligent resource allocation: ensures BookStoreHub runs smoothly even in the face of a flood of avid readers

Cache policies

// Exemple: Cache-Aside Pattern avec Redis
const redis = require('redis');
const client = redis.createClient();

async function getOrderWithCache(orderId) {
  const cacheKey = `order:${orderId}`;
  
  // 1. Vérifier le cache
  const cached = await client.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }
  
  // 2. Miss de cache: aller en base de données
  const order = await db.getOrder(orderId);
  
  // 3. Mettre en cache pour 5 minutes
  await client.setEx(cacheKey, 300, JSON.stringify(order));
  
  return order;
}

async function invalidateOrderCache(orderId) {
  // Invalider le cache lors d'une mise à jour
  await client.del(`order:${orderId}`);
}

4.13 Demo — Optimizing reads via caching in BookStoreHub

Prerequisites

  • MongoDB Community Edition 7.0 installed
  • Redis installed and running

Structure of directory mo04_demo03

mo04_demo03/
├── caching/
│   └── order-service/
│       ├── server.js                ← Version sans cache (baseline)
│       ├── server-with-caching.js   ← Version avec cache Redis
│       └── load-test.yml            ← Script Artillery (100 req/min)
└── sharding/
    └── ...

Demo Methodology

Step 1: Establish the baseline (without cache)

cd caching/order-service
npm install
node server.js  # Démarrer le serveur sans cache

# Dans un autre terminal: créer une commande
curl -X POST http://localhost:3000/orders \
  -H "Content-Type: application/json" \
  -d '{"bookId": "isbn-8345", "quantity": 1}'

# Lancer le load test
artillery run load-test.yml
# Notez: temps de réponse max, p95, p99

Step 2: Measure performance with cache

# Dans un autre terminal: démarrer avec cache
node server-with-caching.js  # Port différent

# Mettre à jour load-test.yml pour pointer sur le nouveau serveur
# Relancer le load test
artillery run load-test.yml
# Comparez les métriques: amélioration significative attendue

Artillery configuration for load test

# load-test.yml
config:
  target: 'http://localhost:3000'
  phases:
    - duration: 60
      arrivalRate: 100   # 100 requêtes par minute
  
scenarios:
  - name: 'Lecture des commandes'
    requests:
      - get:
          url: '/orders/{{ orderId }}'

5. Security Best Practices for Microservices

Module duration: 31m 39s

5.1 Introduction to the Security module

In this module, we will explore how to keep our services secure:

  • Secure communication
  • Manage who can do what in our systems
  • Protecting our data in transit and at rest

Objective: build lines of defense around BookStoreHub and learn best practices to secure our microservices.


5.2 Security Challenges in Microservices

1. Secure communication

In BookStoreHub, let’s imagine that services like order-processing and inventory-management communicate over an insecure channel. This could lead to the interception of sensitive data such as customer information or payment details.

According to industry data, more than 68% of applications could be vulnerable to eavesdropping attacks if they do not use secure communication protocols like HTTPS or mutual TLS.

2. Confidentiality and data protection

With BookStoreHub hosting millions of transactions and user data, ensuring that data at rest and in transit is encrypted is non-negotiable.

Without strong encryption mechanisms, we risk exposure to data breaches, which according to the Verizon Data Breach Investigations report accounted for 36% of all breaches in recent years.

3. Identity and Access Management (IAM)

For BookStoreHub, using Role-Based Access Control (RBAC) ensures that only authorized personnel can access specific data or perform certain operations.

In microservices, this is crucial because it prevents privilege escalation attacks, which increased by 37% last year.


5.3 Authentication and authorization

Two critical concepts, often abbreviated as authn and authz:

  • Authentication (authn): check who someone is
  • Authz (authz): what it is allowed to do

JWT (JSON Web Token) — Authentication

The JWT acts as a signed digital identity card that confirms a user’s identity with each request.

Password breaches constitute 81% of hacking-related breaches (Verizon DBIR report).

// Exemple: Génération d'un JWT
const jwt = require('jsonwebtoken');

function generateToken(user) {
  return jwt.sign(
    {
      userId: user.id,
      email: user.email,
      role: user.role
    },
    process.env.JWT_SECRET,
    { expiresIn: '1h' }
  );
}

// Middleware de vérification JWT
function verifyToken(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Token d\'authentification requis' });
  }
  
  const token = authHeader.substring(7);
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Token invalide ou expiré' });
  }
}

OAuth 2.0 — Authorization

OAuth 2.0 governs what operations an authenticated user is allowed to perform.

// Exemple: Vérification RBAC avec le package accesscontrol
const AccessControl = require('accesscontrol');

const ac = new AccessControl();

// Définir les rôles et permissions
ac.grant('customer')
  .readOwn('order')
  .createOwn('order')
  .readAny('book');

ac.grant('editor')
  .extend('customer')
  .createAny('book')
  .updateAny('book')
  .deleteAny('book');

ac.grant('admin')
  .extend('editor')
  .deleteAny('order');

// Middleware RBAC
function checkPermission(resource, action) {
  return (req, res, next) => {
    const permission = ac.can(req.user.role)[action](resource);
    
    if (!permission.granted) {
      return res.status(403).json({ error: 'Accès refusé' });
    }
    
    next();
  };
}

5.4 Demo — Secure Access with JWT and OAuth2 — Part 1

BookStoreHub security architecture

This demo encapsulates modern web security best practices through:

  • Single Sign-On (SSO)
  • OAuth 2.0 integration
  • Role-Based Access Control (RBAC)

Directory structure

bookstorehub-secure/
├── .env                         ← Secrets HORS du dépôt (credentials GitHub OAuth)
├── identity-provider/           ← Authentification, émet des JWT tokens
│   ├── server.js
│   └── package.json
└── services/
    ├── book-management/         ← Service de gestion des livres
    │   ├── server.js
    │   └── bookRoutes.js
    └── payment-service/        ← Service de paiement
        ├── oauthServer.js      ← Utilise passport.js avec stratégie GitHub
        └── server.js

Best practice: The .env file resides outside of the repository where secrets like OAuth GitHub credentials are stored. It is a cornerstone of secure application development.

Testing authentication

# 1. Démarrer l'identity provider
cd identity-provider
npm install
node server.js

# 2. Se connecter (dans un autre terminal)
curl -X POST http://localhost:4000/login \
  -H "Content-Type: application/json" \
  -d '{"email": "customer@bookstorehub.com", "password": "password123"}'

# Réponse: token JWT valide 1 heure
# { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }

# 3. Démarrer le book-management service
cd services/book-management
node server.js

# 4. Essayer sans token (doit échouer)
curl http://localhost:3001/books/isbn-8345
# Réponse: { "error": "Token d'authentification requis" }

# 5. Essayer avec token
curl http://localhost:3001/books/isbn-8345 \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# Réponse: détails du livre

5.5 Demo — Secure Access with JWT and OAuth2 — Part 2

GitHub OAuth integration for payments

GitHub OAuth configuration:

  1. Create an OAuth application on GitHub (GitHub > Settings > Developer settings > OAuth Apps)
  2. Secure the client_id and the client_secret in the .env file
# .env (exemple)
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
JWT_SECRET=your_super_secret_jwt_key

payment-service with passport.js

// payment-service/oauthServer.js
const passport = require('passport');
const GitHubStrategy = require('passport-github2').Strategy;

// Passport.js - Stratégie GitHub
passport.use(new GitHubStrategy({
  clientID: process.env.GITHUB_CLIENT_ID,
  clientSecret: process.env.GITHUB_CLIENT_SECRET,
  callbackURL: 'http://localhost:3002/auth/github/callback'
}, (accessToken, refreshToken, profile, done) => {
  // Dans un vrai système: stocker/mettre à jour l'utilisateur en BDD
  return done(null, { 
    githubId: profile.id,
    username: profile.username,
    accessToken 
  });
}));

OAuth Payment Flow

Client               BookStoreHub             GitHub
  │                       │                      │
  │── POST /checkout ────▶│                      │
  │                       │── Redirect OAuth ───▶│
  │                       │                      │
  │                       │◀── Code d'autorisation│
  │                       │                      │
  │                       │── Échange code/token▶│
  │                       │◀── Access Token ─────│
  │                       │                      │
  │◀── Confirmation ──────│                      │
  │   paiement réussi     │                      │
# Tester le processus de paiement
curl -X POST http://localhost:3002/checkout \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{"bookId": "isbn-8345", "quantity": 1}'

# Réponse:
# {
#   "success": true,
#   "orderId": "ORD-1704067200000",
#   "message": "Paiement traité avec succès"
# }

5.6 Demo — Secure Access with JWT and OAuth2 — Part 3

RBAC in Action — Role-Based Access Control

In bookRoutes.js, the “add book” endpoint is secured by:

  1. A JWT authentication token
  2. An RBAC check specifying that only an editor can add a book
// bookRoutes.js
const express = require('express');
const AccessControl = require('accesscontrol');
const { verifyToken } = require('../middleware/auth');

const router = express.Router();
const ac = new AccessControl();

// Définition des rôles
ac.grant('customer').readAny('book');
ac.grant('editor').extend('customer').createAny('book').updateAny('book');

// Endpoint protégé par JWT + RBAC
router.post('/books', verifyToken, (req, res) => {
  const permission = ac.can(req.user.role).createAny('book');
  
  if (!permission.granted) {
    return res.status(403).json({ error: 'Accès refusé: rôle éditeur requis' });
  }
  
  // Logique d'ajout du livre...
  console.log('Livre ajouté avec succès!');
  res.json({ message: 'Livre ajouté avec succès', book: req.body });
});

module.exports = router;

Test with different roles

# 1. Se connecter comme customer (rôle: customer)
curl -X POST http://localhost:4000/login \
  -d '{"email": "customer@bookstorehub.com", "password": "pass123"}'
# TOKEN_CUSTOMER = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# 2. Essayer d'ajouter un livre avec le token customer
curl -X POST http://localhost:3001/books \
  -H "Authorization: Bearer $TOKEN_CUSTOMER" \
  -H "Content-Type: application/json" \
  -d '{"isbn": "978-0-13-468599-2", "title": "Nouveau Livre"}'
# Réponse: { "error": "Accès refusé: rôle éditeur requis" }
# ✅ Sécurité fonctionne!

# 3. Se connecter comme editor (rôle: editor)
curl -X POST http://localhost:4000/login \
  -d '{"email": "editor@bookstorehub.com", "password": "editorpass123"}'
# TOKEN_EDITOR = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# 4. Ajouter un livre avec le token editor
curl -X POST http://localhost:3001/books \
  -H "Authorization: Bearer $TOKEN_EDITOR" \
  -H "Content-Type: application/json" \
  -d '{"isbn": "978-0-13-468599-2", "title": "Nouveau Livre"}'
# Réponse: { "message": "Livre ajouté avec succès", "book": {...} }
# ✅ RBAC fonctionne!

5.7 Industry Insights on Microservices Security

Following our journey through the BookStoreHub demo, it is crucial to highlight the insights and practices explored:

  1. Mastering the OWASP Top 10: laying the foundations to understand key vulnerabilities
  2. Follow the Leaders: Stay informed on evolving trends by following experts like Troy Hunt
  3. Engaging with the OWASP Community: Participate in the community’s rich resources
  4. Practical application: start with fundamental security measures in your projects and gradually improve them
  5. Continuous Learning: This continuous learning curve not only elevates your expertise, but also fortifies the digital domain we navigate

5.8 API Gateways — Security Fortresses

API Gateways positioned at the confluence of incoming requests serve as fortified gatekeepers, ensuring that each communication is examined for authenticity and integrity before granting access.

Key Capabilities

1. Traffic management During a promotional event in BookStoreHub, without API Gateway to handle traffic, our services could be overwhelmed. API Gateways intelligently direct requests based on service capacity and priority.

2. Rate Limiting Prevents abuse by limiting the number of requests a user can make within a given time frame. Crucial to avoid exploitation of services and ensure equitable distribution of resources.

// middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // Fenêtre de 15 minutes
  max: 100,                   // Max 100 requêtes par fenêtre par IP
  message: {
    error: 'Trop de requêtes. Veuillez réessayer dans 15 minutes.'
  },
  standardHeaders: true,
  legacyHeaders: false
});

// Limiter plus strict pour les endpoints sensibles
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,
  message: { error: 'Trop de tentatives de connexion.' }
});

module.exports = { apiLimiter, authLimiter };

3. Threat Detection Identification of suspicious patterns and security risks in real time.

// middleware/threatDetection.js
const { body, param, query, validationResult } = require('express-validator');

const sanitizeSearchInput = [
  query('term')
    .isAlphanumeric()
    .withMessage('Terme de recherche invalide: uniquement alphanumériques')
    .trim()
    .escape(),
  
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ 
        error: 'Entrée invalide détectée',
        details: errors.array() 
      });
    }
    next();
  }
];

module.exports = { sanitizeSearchInput };

Recommended library for Node.js: Express Gateway


5.9 Demo — Securing BookStoreHub with API Gateway — Part 1

Structure of the middlewares folder

api-gateway/
├── middlewares/
│   ├── rateLimiter.js      ← express-rate-limit (protection DDoS)
│   └── threatDetection.js  ← express-validator (protection injection SQL, XSS)
├── apiRoutes.js            ← Routage centralisé
├── server.js               ← Intégration du middleware + helmet
└── package.json

server.js — Secure entry point

// server.js
const express = require('express');
const helmet = require('helmet');
const { apiLimiter } = require('./middlewares/rateLimiter');
const { sanitizeSearchInput } = require('./middlewares/threatDetection');
const apiRoutes = require('./apiRoutes');

const app = express();

// helmet: protection HTTP headers (couche de protection OWASP)
app.use(helmet());

// Rate limiting global
app.use('/api/', apiLimiter);

// Corps des requêtes
app.use(express.json());

// Routes avec validation intégrée
app.use('/api', apiRoutes);

app.listen(3000, () => {
  console.log('🔒 API Gateway sécurisée démarrée sur le port 3000');
});

apiRoutes.js — Centralized routing management

// apiRoutes.js
const express = require('express');
const axios = require('axios');
const { sanitizeSearchInput } = require('./middlewares/threatDetection');

const router = express.Router();

// Route de recherche avec validation
router.get('/books/search', sanitizeSearchInput, async (req, res) => {
  try {
    const response = await axios.get(
      `http://localhost:3001/books/search?term=${req.query.term}`
    );
    res.json(response.data);
  } catch (error) {
    res.status(502).json({ error: 'Service indisponible' });
  }
});

// Interface unifiée: un seul point d'entrée, plusieurs services derrière
router.get('/books/:id', async (req, res) => {
  const response = await axios.get(`http://localhost:3001/books/${req.params.id}`);
  res.json(response.data);
});

router.post('/orders', async (req, res) => {
  const response = await axios.post('http://localhost:3002/orders', req.body);
  res.json(response.data);
});

module.exports = router;

Note: By incorporating helmet, we adopt an industry-recommended layer of protection that defines secure HTTP headers — a simple but effective barrier against many web vulnerabilities.


5.10 Demo — Securing BookStoreHub with API Gateway — Part 2

Rate Limiting Demonstration

During a highly anticipated book release, BookStoreHub faces a surge of interest. To ensure that our services remain fluid and responsive for everyone, we have implemented rate limiting.

Rate limiting test:

# Première requête - succès
curl http://localhost:3000/api/books/isbn-8345
# Réponse: détails du livre

# Après X requêtes rapides (dépassement du seuil)
curl http://localhost:3000/api/books/isbn-8345
# Réponse: {
#   "error": "Trop de requêtes. Veuillez réessayer dans 15 minutes."
# }

To delve deeper into web traffic management in a microservices environment:

  • Kong: robust API management platform
  • Tyk: effective traffic management, security and monitoring tool

Staying informed via community forums and adhering to security guidelines established by OWASP is crucial.


5.11 Demo — Securing BookStoreHub with API Gateway — Part 3

Threat Detection Demonstration

Our middleware threatDetection.js uses express-validator to probe incoming search requests. The validation rule ensures that the search term is alphanumeric — a simple but effective barrier against injection attacks.

SQL Injection attack attempt:

# Tentative d'injection SQL classique
curl "http://localhost:3000/api/books/search?term=1' OR '1'='1"

# Réponse du système:
# {
#   "error": "Entrée invalide détectée",
#   "details": [{
#     "msg": "Terme de recherche invalide: uniquement alphanumériques",
#     "param": "term",
#     "location": "query"
#   }]
# }

This attempt attempts to manipulate our database query to return all records by making the condition always true — which could expose sensitive information.

Key Lesson: Incorporating input validation directly into our API Gateway adds a critical layer of security, ensuring that only clean, validated data is allowed to pass through to our microservices. This strategy is part of a defense-in-depth approach where multiple security measures work together to protect our system.

Always remember: sanitizing and validating user inputs, particularly those interfacing with databases or sensitive systems, is non-negotiable.


5.12 Encryption and secure communication

In BookStoreHub where a lot of data passes through the network, keeping the information secure during transmission is very important.

HTTPS

HTTPS is a key part of secure data transmission. It encrypts information exchanged between the user’s browser and BookStoreHub — sensitive customer details and transactions cannot be intercepted.

When you see the padlock icon in your browser, HTTPS is used. It’s not just a best practice — it’s a promise of security and trust between a service and its users.

Mutual TLS (mTLS)

When different parts of BookStoreHub communicate with each other, they use mutual TLS. This further enhances security by having both the client AND the server prove their identity to each other.

Sans mTLS:           Avec mTLS:
Client ──────▶ Server    Client ◀──▶ Server
(seulement le serveur    (les deux s'authentifient
 s'authentifie)           mutuellement)

Data encryption at rest

Encrypt data whether stored or transmitted. By using advanced encryption methods, BookStoreHub ensures that sensitive information — like customers’ personal details or payment information — cannot be read by anyone who should not have access to it.

Node.js libraries:

  • node-forge: encryption tools
  • https module (integrated): creation of secure HTTPS servers

Reference: Cryptography and Network Security by William Stallings


5.13 Demo — Secure web communication in BookStoreHub

Problem addressed

Risk of compromised data in transit — resolved by implementing HTTPS.

New file: httpsServer.js

// api-gateway/httpsServer.js
const https = require('https');
const fs = require('fs');
const app = require('./app');

// Chargement des certificats SSL
const options = {
  key: fs.readFileSync(process.env.SSL_KEY_PATH || './certs/server.key'),
  cert: fs.readFileSync(process.env.SSL_CERT_PATH || './certs/server.cert')
};

// Créer le serveur HTTPS sécurisé
const server = https.createServer(options, app);

server.listen(443, () => {
  console.log('🔒 Serveur HTTPS démarré sur le port 443');
});

Generating self-signed certificates (for local tests)

# Générer certificats auto-signés pour le développement local
openssl req -x509 -newkey rsa:4096 \
  -keyout ./certs/server.key \
  -out ./certs/server.cert \
  -days 365 \
  -nodes \
  -subj "/C=CA/ST=Quebec/L=Montreal/O=BookStoreHub/CN=localhost"

IMPORTANT: In production, certificates would be obtained from a Certification Authority (CA), and should be stored in a secure location, NEVER directly in the project directory to avoid accidental exposure.

Testing the HTTPS connection

# Démarrer le book-management service
cd services/book-management
node server.js

# Démarrer l'API Gateway HTTPS
cd api-gateway
node httpsServer.js
# "🔒 Serveur HTTPS démarré sur le port 443"

# Test avec curl (flag -k pour bypass validation SSL auto-signé en dev)
curl -k https://localhost:443/api/books/search?term=microservices
# Réponse JSON: détails des livres via connexion HTTPS sécurisée

5.14 Encryption Best Practices for Microservices

Secure certificate management:

  • Always store your keys and certificates outside of your codebase
  • Preferably in a secure secrets management tool or service
  • Use environment variables to reference them in your application

Certificate renewal automation:

  • Consider automating certificate renewal and deployment in production
  • Let’s Encrypt offers free certificates with automated tools to keep them up to date

Safety checklist:

PracticalDescription
HTTPS everywhereAll endpoints use HTTPS
mTLS between servicesSecure service-to-service communication
Certificate rotationAutomated renewal before expiration
Secrets outside the codeEnvironment variables or vault
OWASP Top 10Regular review of security posture
Secure HTTP Headershelmet.js configured
Security auditParticipation in security forums

Reminder: Encryption isn’t just a feature — it’s a commitment to the privacy and security of your users. As you continue to build and improve your applications, keep security at the forefront. Always explore, implement, and iterate on your security measures.


6. Monolith to Microservices Refactoring Techniques

Module duration: 37m 23s

6.1 Introduction to the Refactoring module

This final module focuses on the transition from monolithic architectures to microservices in the Node.js ecosystem. We will cover essential techniques and patterns:

  • The Strangler Pattern
  • Event-Driven Decomposition

Primary objective: understand the flexibility, scalability and resilience that microservices bring to software development.

Through detailed explanations and practical examples with BookStoreHub, we’ll dive into how these strategies facilitate agile and efficient system refactoring.


6.2 Monolithic architecture

The monolithic architecture can be visualized as a single interconnected application tier where all components are unified — a one-stop shop.

Key Features

1. Centralized system Like building a building with just one foundation. For the initial BookStoreHub, this meant that inventory management, user authentication, and transaction processing were all part of a single codebase.

2. Unified codebase Simple unified deployment — a single update or patch deploys to all services simultaneously. When BookStoreHub’s search functionality needed an upgrade, the entire platform had to be redeployed, even if no other functionality was altered.

3. The compromise of simplicity Starting with a monolithic architecture provides simplicity in the initial deployment phases. However, this simplicity comes at a cost in terms of scalability. Adding new titles or features became a challenge — scaling was hampered by its interconnected nature.

Architecture Monolithique BookStoreHub:
┌──────────────────────────────────────────────────────────┐
│                    BookStoreHub Monolithe                  │
│                                                            │
│  ┌─────────────┐  ┌──────────────┐  ┌────────────────┐   │
│  │ User Auth   │  │   Inventory  │  │ Order Process  │   │
│  │             │  │ Management   │  │                │   │
│  └─────────────┘  └──────────────┘  └────────────────┘   │
│  ┌─────────────┐  ┌──────────────┐  ┌────────────────┐   │
│  │Book Reviews │  │Recommendations│  │  Search &      │   │
│  │             │  │              │  │  Catalog       │   │
│  └─────────────┘  └──────────────┘  └────────────────┘   │
│                                                            │
│  Tout dans un seul processus, une seule base de données   │
└──────────────────────────────────────────────────────────┘

6.3 Microservices architecture

Microservices architecture envisions applications like BookStoreHub as a decentralized ecosystem where functions like inventory management and user authentication are reimaged as a suite of independently deployable, loosely coupled services.

Fundamentals

1. Autonomy of services According to Conway’s Law, software systems reflect the communication structures of the organizations that design them. In BookStoreHub, if the team responsible for user authentication operates independently from the team managing inventory, this naturally leads to the development of separate autonomous services.

2. Scalable components When a new release causes a traffic spike, the inventory management service can be scaled on demand without needing to scale the entire application.

3. Complexity Balance As Fred Brooks points out in The Mythical Man-Month — microservices, while facilitating scalability and resilience, increase the complexity of coordinating services.

Architecture Microservices BookStoreHub:
                        ┌─────────────────┐
                        │   API Gateway   │
                        └────────┬────────┘
                                 │
          ┌──────────────────────┼────────────────────────┐
          │                      │                        │
          ▼                      ▼                        ▼
┌──────────────────┐   ┌──────────────────┐   ┌──────────────────┐
│  Auth Service    │   │Inventory Service │   │  Order Service   │
│  (port 3001)     │   │  (port 3002)     │   │  (port 3003)     │
│  Base de données │   │  Base de données │   │  Base de données │
│  propre          │   │  propre          │   │  propre          │
└──────────────────┘   └──────────────────┘   └──────────────────┘
          │                      │                        │
          ▼                      ▼                        ▼
┌──────────────────┐   ┌──────────────────┐
│ Review Service   │   │Recommend. Service│
│  (port 3004)     │   │  (port 3005)     │
└──────────────────┘   └──────────────────┘

6.4 The Strangler Pattern

The Strangler Pattern is inspired by the concept where a strangler vine gradually surrounds and replaces a tree — metaphorically representing the gradual modernization of legacy systems by introducing new features alongside them.

Incremental renovation strategy

Like the methodical phases of urban renewal where updates are executed in deliberate, controlled segments. For example, the gradual migration of BookStoreHub’s user authentication system:

  1. Monolithic systems and microservices work in parallel
  2. Gradual rerouting of traffic to the new system
  3. Staged approach minimizing service interruptions

Legacy and new system coexistence

The transition ensures that during the migration, the old and new systems work at the same time. For example, BookStoreHub’s checkout process can still be processed through the old system until the new checkout service is fully operational and reliable.

Risk management

  • Any component of the new microservices architecture can be isolated and evaluated in the production environment without compromising the overall functionality of the platform
  • If a new feature encounters problems, the platform can fall back to the legacy system

Key difference:

StrategyDescriptionUse cases
Incremental renovationStep-by-step improvement within the existing frameworkNon-critical, loosely coupled components (e.g. recommendation engine)
Legacy+new coexistenceParallel execution of old and new systemsCritical components (e.g. payment processing)

6.5 Event-Driven Decomposition

This strategy involves breaking down the monolithic architecture of applications like BookStoreHub into microservices, driven by underlying business events and domains. It is a method that aligns with the natural activities occurring in the application.

1. Domain Partitioning

Imagine BookStoreHub as a bustling marketplace. Just as different sections of the market specialize in specific goods, domain partitioning divides the system by specific business functional areas.

Domains identified:

  • order-management: order management with its own processes and data models
  • inventory-tracking: tracking the inventory with its specificities

This division not only simplifies the complexity of the system, but also improves the focus on the quality and performance of individual services.

2. Event Identification

Each action in BookStoreHub triggers specific events:

  • Add book to cart → ITEM_ADDED_TO_CART event
  • Complete a purchase → PURCHASE_COMPLETED event
  • Post a review → REVIEW_POSTED event

It’s like mapping the heartbeat of the system — understanding what drives different parts of the application.

3. Capability Segregation

This principle uses insights from domain partitioning and event identification to tailor services to distinct functional capabilities.

// Exemple: Décomposition basée sur les événements
const EventEmitter = require('events');
const bookstoreEvents = new EventEmitter();

// Gestionnaires d'événements par domaine
bookstoreEvents.on('ORDER_CREATED', async (order) => {
  // order-service traite la commande
  await orderService.process(order);
});

bookstoreEvents.on('ORDER_CREATED', async (order) => {
  // inventory-service déduit le stock
  await inventoryService.deductStock(order.items);
});

bookstoreEvents.on('PAYMENT_COMPLETED', async (payment) => {
  // notification-service envoie confirmation
  await notificationService.sendConfirmation(payment);
});

6.6 Demo — Refactoring with the Strangler Pattern

Prerequisites

  • Docker Desktop running
  • kubectl installed (interaction with Kubernetes cluster)
  • Minikube configured (local Kubernetes environment)

Before refactoring — Monolithic structure

bookstore-monolith/
├── src/
│   ├── auth/           ← Authentification
│   ├── inventory/      ← Inventaire  
│   └── orders/         ← Commandes
├── app.js              ← Tout dans un seul codebase
└── package.json

After refactoring — Microservices architecture

bookstore-microservices/
├── auth-service/
│   ├── app.js          ← Port 3001
│   ├── Dockerfile
│   └── package.json
├── inventory-service/
│   ├── app.js          ← Port 3002
│   ├── Dockerfile
│   └── package.json
└── kubernetes/
    ├── auth-deployment.yml
    ├── auth-service.yml
    ├── inventory-deployment.yml
    └── inventory-service.yml

Statistics show that incremental changes reduce the error rate by 30% compared to “big bang” deployments.

Dockerfiles

# auth-service/Dockerfile
FROM node:21-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3001
CMD ["node", "app.js"]

Kubernetes Configuration

# kubernetes/auth-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: auth-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: auth-service
  template:
    metadata:
      labels:
        app: auth-service
    spec:
      containers:
      - name: auth-service
        image: bookstorehub/auth-service:latest
        ports:
        - containerPort: 3001
        env:
        - name: JWT_SECRET
          valueFrom:
            secretKeyRef:
              name: bookstore-secrets
              key: jwt-secret
---
apiVersion: v1
kind: Service
metadata:
  name: auth-service
spec:
  selector:
    app: auth-service
  ports:
  - port: 3001
    targetPort: 3001

Deployment process

# 1. Démarrer Minikube
minikube start

# 2. Construire les images Docker dans l'environnement Docker de Minikube
eval $(minikube docker-env)
docker build -t bookstorehub/auth-service:latest ./auth-service
docker build -t bookstorehub/inventory-service:latest ./inventory-service

# 3. Déployer dans Kubernetes
kubectl apply -f kubernetes/

# 4. Vérifier le déploiement
kubectl get pods
kubectl get services

# 5. Port forwarding pour accéder aux services (simule le Strangler Pattern)
kubectl port-forward service/auth-service 3001:3001 &
kubectl port-forward service/inventory-service 3002:3002 &

# 6. Tester les endpoints
curl http://localhost:3001/auth/login \
  -X POST \
  -d '{"email": "test@bookstore.com", "password": "pass123"}'

curl http://localhost:3002/inventory/books

6.7 Unification of DDD and Clean Code principles

As we navigate further refinement of our microservices architecture for BookStoreHub, it becomes imperative to integrate the guiding principles of DDD with Object Oriented Programming (OOP) and Clean Code practices.

1. Domain Modeling

Design software models that accurately reflect real-world business domains. As with the inventory system in BookStoreHub, align our model closely with the business domain of inventory management.

Reference: Domain-Driven Design by Eric Evans — the importance of a shared language between developers and domain experts.

2. Encapsulation and Modularity

Build standalone modules that encapsulate specific functionality. Each microservice in BookStoreHub — whether it deals with user authentication or payment processing — operates as an independent module, encapsulating its domain logic.

Reference: Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin — clear, concise code that is easy to understand, modify and extend.

3. Integration of Clean Code practices

// AVANT - Code monolithique avec couplage fort
class BookStoreHub {
  addBook(isbn, title, price, stock, authorId, categoryId) {
    // Validation inline
    if (!isbn || isbn.length < 10) throw new Error('ISBN invalide');
    if (price < 0) throw new Error('Prix invalide');
    if (stock < 0) throw new Error('Stock invalide');
    
    // Appels directs à d'autres domaines
    const author = db.query(`SELECT * FROM authors WHERE id = ${authorId}`);
    const category = db.query(`SELECT * FROM categories WHERE id = ${categoryId}`);
    
    return db.query(`INSERT INTO books...`);
  }
}

// APRÈS - Clean Code avec DDD
class BookInventoryService {
  constructor(bookRepository, authorRepository) {
    this._bookRepository = bookRepository;
    this._authorRepository = authorRepository;
  }
  
  async addBook(bookData) {
    // Value Objects pour validation encapsulée
    const isbn = new ISBN(bookData.isbn);
    const price = new Price(bookData.price);
    
    // Aggregate qui gère la logique métier
    const book = Book.create({ isbn, price, ...bookData });
    
    await this._bookRepository.save(book);
    
    // Publier l'événement domaine
    this._publishEvent(new BookAddedEvent(book));
    
    return book;
  }
}

6.8 Agile and DDD in refactoring

The synergy between agile methodologies and DDD elevates both the flexibility and precision of refactoring processes.

1. Iterative refactoring

Inspired by the agile principle of iterative progress, we adopt incremental and regular updates. Like growing a garden where constant care promotes growth and resilience.

Applying this to BookStoreHub:

  • Focus on improving the checkout process step by step
  • Maintaining a Robust and Adaptable System

Reference: Refactoring: Improving the Design of Existing Code by Martin Fowler — the importance of small, focused changes to system integrity.

2. Feedback Loops

In BookStoreHub, gathering insights from end users AND development teams powers each refinement cycle.

Reference: Scrum: The Art of Doing Twice the Work in Half the Time by Jeff Sutherland — the power of feedback to drive innovation and excellence.

3. Focus on collaboration

Agile and DDD thrive on the collaborative synergy of cross-functional teams, merging the expertise of developers, software architects, and business experts.

Result: – Solutions that are both technically sound and aligned with business objectives

  • Facilitating a culture of continual code and architecture review

6.9 Expert Tips for Refactoring Large Monoliths

AdviceDescription
Start smallStart by breaking down a less critical or smaller service to gain insights and confidence
Focus on communicationEnsure clear channels for team communication and engagement with domain experts
Automate testsPrioritize Automated Testing to Maintain High Code Quality During Transitions
Monitor and measureContinuously monitor performance and user feedback to guide refinements

By marrying agile practices with DDD in the sphere of refactoring, we not only strengthen the adaptability and precision of the system, but also align ourselves with the standards of excellence to develop scalable and maintainable microservice architectures.


6.10 Refactoring Challenges

As we continue our journey of transforming from monolithic architectures to microservices, it is essential to address the challenges we may encounter.

1. Complex dependencies

Monolithic systems often house tightly interwoven components — like a woven tapestry where pulling a thread could unravel the entire structure.

Solution: use DDD to delineate clear boundaries, breaking down systems into manageable subdomains.

Reference: Martin Fowler suggests breaking these systems down into manageable subdomains.

2. Data consistency

By breaking BookStoreHub into microservices, ensuring data integrity across these newly isolated services becomes paramount.

Technical:

  • Eventual Consistency: possible consistency between services
  • Distributed Transaction Patterns: distributed transaction patterns
  • Apache Kafka for event-driven data management

Reference: Gregor Hohpe in Enterprise Integration Patterns.

// Exemple: Saga Pattern pour les transactions distribuées
class OrderSaga {
  async execute(orderData) {
    const sagaId = uuid.v4();
    
    try {
      // Étape 1: Créer la commande
      const order = await orderService.create(orderData, sagaId);
      
      // Étape 2: Déduire le stock
      await inventoryService.reserve(order.items, sagaId);
      
      // Étape 3: Traiter le paiement
      await paymentService.charge(order.total, sagaId);
      
      // Étape 4: Confirmer la commande
      await orderService.confirm(order.id);
      
    } catch (error) {
      // Compensation (rollback distribué)
      await this.compensate(sagaId, error);
      throw error;
    }
  }
  
  async compensate(sagaId, error) {
    // Annuler dans l'ordre inverse
    await paymentService.refund(sagaId);
    await inventoryService.release(sagaId);
    await orderService.cancel(sagaId);
  }
}

3. Advanced testing strategies

Traditional testing approaches may be insufficient in a distributed environment.

New approaches:

  • Contract Testing: ensure that each microservice respects its contracts
  • End-to-End Integration Testing: verify that the system operates consistently

Reference: The Coding Dojo Handbook by Emily Bache to improve testing practices.

Tools:

  • Pact for contract testing
  • Cypress for E2E testing
  • CI/CD pipeline for continuous testing and integration

6.11 Microservices Refactoring Essentials

A set of fundamental practices guide the successful transition to a microservices architecture.

1. Isolate and containerize

Separate individual components and wrap them in containers. This isolation ensures that each microservice can be independently developed, deployed, and scaled.

According to the Cloud Native Computing Foundation, the use of container technologies like Docker has grown exponentially with Kubernetes becoming the de facto standard.

2. Set domain boundaries

Establishing clear domain boundaries is fundamental. This practice, inspired by DDD, aligns each microservice with specific business functions.

Reference: Eric Evans in his seminal book on DDD.

3. Centralize and simplify

Adopting an API Gateway centralizes external access, improving security and simplifying the customer interface. This not only improves the user experience, but also secures the microservices against direct external access.

4. Identify and eliminate dependencies

Vigilance in identifying and eliminating tight coupling between services is key to maintaining architectural modularity. Regular dependency reviews help prevent the emergence of a distributed monolith.

5. Monitor and optimize

With microservices, monitoring performance and continually optimizing is not optional. Tools like Prometheus, Grafana, and distributed tracing services like Jaeger or Zipkin provide essential insights.


6.12 Best practices in refactoring

These specific strategies catalyze a successful and smooth transition to a microservices framework.

1. Incremental refactoring

The cornerstone of our strategy. This approach advocates making small, manageable updates to the codebase.

According to the 2020 Accelerate State of DevOps report, teams that practice incremental change are twice as likely to achieve their business goals.

2. CI/CD (Continuous Integration / Continuous Deployment)

The backbone of modern software development, ensuring that code changes are automatically built, tested and prepared for production deployment.

According to the Puppet 2021 State of DevOps Report, the adoption of CI/CD practices is associated with high-performing teams with:

  • Faster deployment frequencies
  • Shorter lead times for changes
  • Lower change failure rates

3. Clear documentation

Amid the complexities of transitioning to microservices, clear and detailed documentation emerges as a beacon of clarity. This practice ensures that every aspect of the system—from its architecture to individual department responsibilities—is thoroughly documented.

4. Summary of Best Practices

PracticalImpact
Incremental refactoringRisk reduction, manageable changes
CI/CDFaster deployments, improved quality
Clear documentationBetter maintenance, easier onboarding
Automated testsEarly problem detection
Continuous monitoringPerformance visibility
Code reviewsKnowledge sharing, quality

6.13 Next steps

We’ve covered a lot of ground in this course. Here are the next steps to continue your learning journey in the world of microservices:

BookAuthorSubject
Building MicroservicesSam NewmanPractical advice on the design, construction and maintenance of complex systems
Implementing Domain-Driven DesignVaughn VernonIn-Depth Guide to DDD Principles Integral to Microservices Architectures
Domain-Driven Design: Tackling ComplexitiesEric EvansThe basics of DDD, pioneer of the concept
Refactoring: Improving the DesignMartin FowlerRefactoring Principles
Clean CodeRobert C. MartinClean and maintainable code
The Mythical Man-MonthFred BrooksComplexity in software engineering
Enterprise Integration PatternsGregor HohpeIntegration Patterns for Distributed Systems
The Coding Dojo HandbookEmily BacheImproved testing practices
Scrum: The Art of Doing Twice the WorkJeff SutherlandScrum Methodology
Cryptography and Network SecurityWilliam StallingsSecurity and encryption

People to follow

  • Martin Fowler: Industry leader in software architecture
  • Eric Evans: pioneer of Domain-Driven Design

Get involved in the community

  • Stack Overflow and Reddit: share experiences, ask questions, offer help
  • LinkedIn Groups: network and learn from professionals in the field

Practical learning

  • Nothing beats learning by doing — take the jump and work on real projects
  • Apply what you have learned, experiment, and build your own microservices

Stay up to date

  • Attend conferences (KubeCon, NodeConf, etc.)
  • Participate in workshops
  • Keep learning — tech evolves quickly

Final reminder: This is only the beginning. The path to microservices mastery is continuous and evolving. Keep reading, keep learning, and keep coding. Good luck, and see you soon in the tech community!


7. Appendix: Summary of technologies used

TechnologyRole in the course
Node.js 21Main Runtime
Express.jsWeb framework
RedisIn-memory cache (CQRS read model)
RabbitMQMessage broker (CQRS write queue)
Apache KafkaEvent Streaming (Event Sourcing)
MongoDBRead-optimized database
PostgreSQLWrite-secure database
NGINXLoad balancer software
ConsulService Discovery (HashiCorp)
DockerContainerization
Kubernetes / MinikubeContainer Orchestration
JWTStateless Authentication
OAuth 2.0Delegated authorization
passport.jsNode.js authentication (GitHub strategy)
accesscontrolRBAC (Role-Based Access Control)
express-rate-limitRate limiting (DDoS protection)
express-validatorValidation of inputs (injection protection)
helmetSecure HTTP headers
ArtilleryLoad testing
Let’s EncryptAutomated SSL/TLS certificates
OpenSSLCertificate generation (dev)

Search Terms

node.js · microservices · topics · apis · backend · full-stack · web · refactoring · ddd · bookstorehub · load · configuration · cqrs · event · directory · prerequisites · secure · service · architecture · context · security · access · aggregates · balancers

Interested in this course?

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