Node.js version: 21.6.1 (compatible with Node.js 21)
Table of Contents
- 2.1 Course introduction and prerequisites
- 2.2 Introduction to Domain-Driven Design (DDD)
- 2.3 Ubiquitous Language
- 2.4 Monoliths and microservices
- 2.5 Bounded Contexts in microservices
- 2.6 Bounded Context in depth
- 2.7 Context Mapping
- 2.8 Strategic Domain-Driven Design
- 2.9 Demo — Define Bounded Contexts in BookStoreHub
- 2.10 Working with Aggregates and Entities
- 2.11 Aggregates
- 2.12 Entities
- 2.13 Life cycle of Aggregates and Entities in DDD
- 2.14 Collaboration between Aggregates and Entities
- 2.15 Demo — Identify Aggregates and Entities in BookStoreHub
- 2.16 Value Objects in DDD
- 2.17 Value Objects in depth
- 2.18 The importance of immutability in DDD
- 2.19 Demo — Identify Value Objects in BookStoreHub
- 2.20 DDD and Processes — A Symbiotic Relationship
- 2.21 DDD in Agile / Scrum
- 3.1 Introduction to CQRS module
- 3.2 Understanding CQRS
- 3.3 Event Sourcing in microservices
- 3.4 Read Models — Create projections with CQRS
- 3.5 Checklist of projections and their essence
- 3.6 Messaging integration in microservices
- 3.7 CQRS in action — The Book of the Month feature
- 3.8 Architecture Assessment Checklist
- 3.9 CQRS implementation for Book of the Month functionality
- 3.10 Demo — Optimizing reads with Redis and CQRS
- 3.11 Demo — Resilient writes with RabbitMQ
- 3.12 System Architecture Overview
- 3.13 Demo — Event Sourcing in practice
- 4.1 Introduction to Scaling module
- 4.2 Introduction to microservices scaling
- 4.3 Mastering load balancing in microservices
- 4.4 Compare software and hardware load balancers
- 4.5 Cloud Load Balancers
- 4.6 Ensuring system health in microservices
- 4.7 Demo — Adding a load balancer in BookStoreHub
- 4.8 Service Discovery — The microservices compass
- 4.9 Service Registry Mechanics
- 4.10 Demo — Implement dynamic Service Discovery in BookStoreHub
- 4.11 Database Sharding
- 4.12 Caching techniques
- 4.13 Demo — Optimizing reads via caching in BookStoreHub
- 5.1 Introduction to the Security module
- 5.2 Security Challenges in Microservices
- 5.3 Authentication and authorization
- 5.4 Demo — Secure access with JWT and OAuth2 — Part 1
- 5.5 Demo — Secure access with JWT and OAuth2 — Part 2
- 5.6 Demo — Secure access with JWT and OAuth2 — Part 3
- 5.7 Industry Insights on Microservices Security
- 5.8 API Gateways — Security Fortresses
- 5.9 Demo — Securing BookStoreHub with API Gateway — Part 1
- 5.10 Demo — Securing BookStoreHub with API Gateway — Part 2
- 5.11 Demo — Securing BookStoreHub with API Gateway — Part 3
- 5.12 Encryption and secure communication
- 5.13 Demo — Secure web communication in BookStoreHub
- 5.14 Encryption Best Practices for Microservices
- 6.1 Introduction to the Refactoring module
- 6.2 Monolithic architecture
- 6.3 Microservices architecture
- 6.4 The Strangler Pattern
- 6.5 Event-Driven Decomposition
- 6.6 Demo — Refactoring with the Strangler Pattern
- 6.7 Unification of DDD and Clean Code principles
- 6.8 Agile and DDD in refactoring
- 6.9 Expert Tips for Refactoring Large Monoliths
- 6.10 Refactoring Challenges
- 6.11 Microservices Refactoring Essentials
- 6.12 Refactoring Best Practices
- 6.13 Next steps
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:
- Implementing Domain-Driven Design (DDD)
- The use of CQRS and Event Sourcing
- scaling and optimization of your microservices
- Mastering best security practices for microservices
- 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 reviewsrequires real-time feedback - Bounded context
book inventoryneeds 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 Context | Role |
|---|---|
order-processing | Create and process orders |
book-inventory | Manage and catalog stock |
customer-reviews | Customer Reviews & Ratings |
recommendation-system | Book 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:
-
Data integrity: This grouping maintains data integrity — when we update a detail, the associated information remains consistent.
-
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.
-
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:
-
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.
-
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.
-
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.
-
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
| Phase | Description | Example |
|---|---|---|
| Creation | Via a factory or a manufacturer | Creation of the “Science Fiction” category |
| Edit | Compliance with business rules and consistency | Updated to include a subgenre |
| Delete | Explicit or cascading | Deleting a category reclassifies all associated books |
Entity life cycle
| Phase | Description | Example |
|---|---|---|
| Creation | Within an aggregate or independently | Added Cosmos by Carl Sagan |
| Edit | Within or independently of the aggregate | Adjusted the price of Cosmos without impacting the category |
| Delete | Explicit or cascading | Deleting 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):
- Test
addBookviolates the DRY principle by overlapping validations withupdateBookPrice - Direct manipulation of stock to a negative value leads to inconsistency
- Category-related testing reveals direct booklist manipulation, bypassing protocols
- 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 listfor the book collection seasonfor 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:
| Criterion | Description |
|---|---|
| Sync with domain events | Keep projections up to date |
| Regular check | Check and optimize the structures of read models |
| Capacity Assessment | Will 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:
| Criterion | Objective | Status |
|---|---|---|
| Scalability | Manage 2000 RPS for reads, queue for writes | ✅ Smart scaling |
| Performance | Response time < 2 seconds during peak | ✅ Mandatory |
| Resilience | Withstand peak traffic without failure | ✅ Bullet-proof architecture |
| Maintainability | Clean and manageable codebase | ✅ CQRS separates commands and queries |
| Decoupling | Services operating independently | ✅ CQRS facilitates smooth interaction |
| Data consistency | Users 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:
| Step | Description |
|---|---|
| 1 | Book of the Month announcement (trigger) |
| 2 | User activity surge |
| 3 | Increased read operations |
| 4 | Increased write operations |
| 5 | Scaling read models for speed and efficiency |
| 6 | Separate queue for write operations |
| 7 | MongoDB for fast reads |
| 8 | PostgreSQL for secure writes |
| 9 | Real-time data synchronization |
| 10 | Scaling read and write services independently |
| 11 | Read service scaling |
| 12 | Scaling of the write service |
| 13 | Maintaining 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:
| Component | Role |
|---|---|
| Redis | In-memory data store — ultra-fast data retrieval for Read Service |
| Read Service | Fast data delivery without delay |
| Write Service | Captures user inputs accurately, respecting the exact order of transactions |
| RabbitMQ | Orderly queue — handles high-throughput loads, preserves order of operations, ensures reliable message delivery |
| Kafka | Robust 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
| Type | Features |
|---|---|
| Software | Very adaptable, integrates with the cloud, solutions: NGINX, HAProxy, Traefik |
| Hardware | Raw power, for traditional on-premises data centers: F5 BIG-IP, Citrix |
Distribution algorithms
| Algorithm | Description | Use cases |
|---|---|---|
| Round Robin | Fair rotation, passes each request to the next server | Uniform load |
| Least Connections | Prioritize servers with fewer active connections | Long-running queries |
| IP Hash | Directs a user to the same server based on their IP | User 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:
| Supplier | Services | Features |
|---|---|---|
| AWS | Amazon ELB (Elastic Load Balancing) | Integrates seamlessly with AWS services, resilient and auto-scaling solution |
| Microsoft Azure | Azure Load Balancer | Azure native choice, high availability and security in the Azure ecosystem |
| Google Cloud | Google Cloud Load Balancer | Global 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
| Method | Description | Analogy |
|---|---|---|
| HTTP | Quick pulse check | Rapid pulse test |
| TCP | Reflex test | Reflex test |
| Custom scripts | Complete health checkup | Complete 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
| Approach | Advantages | Disadvantages |
|---|---|---|
| Discovery Service | Real-time update, adaptive, dynamic | More complex to configure |
| Blue/Green Deployment | Effective for transitions | Does not maintain a continuous record of services |
| A/B Deployment | Useful for testing | Same |
| Local DLLs and JARs | Simple | Fixed 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
- Dramatically reduced response times: the most requested data is at your fingertips
- Smart cache policy: like a librarian who knows exactly when to replace old editions with new ones — cache validity management
- Reducing database load: During peak hours, caching is like an expert crowd manager, easing database load and conserving resources
- 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
.envfile 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:
- Create an OAuth application on GitHub (GitHub > Settings > Developer settings > OAuth Apps)
- Secure the
client_idand theclient_secretin the.envfile
# .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:
- A JWT authentication token
- 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:
- Mastering the OWASP Top 10: laying the foundations to understand key vulnerabilities
- Follow the Leaders: Stay informed on evolving trends by following experts like Troy Hunt
- Engaging with the OWASP Community: Participate in the community’s rich resources
- Practical application: start with fundamental security measures in your projects and gradually improve them
- 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 toolshttpsmodule (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:
| Practical | Description |
|---|---|
| HTTPS everywhere | All endpoints use HTTPS |
| mTLS between services | Secure service-to-service communication |
| Certificate rotation | Automated renewal before expiration |
| Secrets outside the code | Environment variables or vault |
| OWASP Top 10 | Regular review of security posture |
| Secure HTTP Headers | helmet.js configured |
| Security audit | Participation 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:
- Monolithic systems and microservices work in parallel
- Gradual rerouting of traffic to the new system
- 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:
| Strategy | Description | Use cases |
|---|---|---|
| Incremental renovation | Step-by-step improvement within the existing framework | Non-critical, loosely coupled components (e.g. recommendation engine) |
| Legacy+new coexistence | Parallel execution of old and new systems | Critical 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 modelsinventory-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_CARTevent - Complete a purchase →
PURCHASE_COMPLETEDevent - Post a review →
REVIEW_POSTEDevent
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
| Advice | Description |
|---|---|
| Start small | Start by breaking down a less critical or smaller service to gain insights and confidence |
| Focus on communication | Ensure clear channels for team communication and engagement with domain experts |
| Automate tests | Prioritize Automated Testing to Maintain High Code Quality During Transitions |
| Monitor and measure | Continuously 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
| Practical | Impact |
|---|---|
| Incremental refactoring | Risk reduction, manageable changes |
| CI/CD | Faster deployments, improved quality |
| Clear documentation | Better maintenance, easier onboarding |
| Automated tests | Early problem detection |
| Continuous monitoring | Performance visibility |
| Code reviews | Knowledge 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:
Recommended reading
| Book | Author | Subject |
|---|---|---|
| Building Microservices | Sam Newman | Practical advice on the design, construction and maintenance of complex systems |
| Implementing Domain-Driven Design | Vaughn Vernon | In-Depth Guide to DDD Principles Integral to Microservices Architectures |
| Domain-Driven Design: Tackling Complexities | Eric Evans | The basics of DDD, pioneer of the concept |
| Refactoring: Improving the Design | Martin Fowler | Refactoring Principles |
| Clean Code | Robert C. Martin | Clean and maintainable code |
| The Mythical Man-Month | Fred Brooks | Complexity in software engineering |
| Enterprise Integration Patterns | Gregor Hohpe | Integration Patterns for Distributed Systems |
| The Coding Dojo Handbook | Emily Bache | Improved testing practices |
| Scrum: The Art of Doing Twice the Work | Jeff Sutherland | Scrum Methodology |
| Cryptography and Network Security | William Stallings | Security 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
| Technology | Role in the course |
|---|---|
| Node.js 21 | Main Runtime |
| Express.js | Web framework |
| Redis | In-memory cache (CQRS read model) |
| RabbitMQ | Message broker (CQRS write queue) |
| Apache Kafka | Event Streaming (Event Sourcing) |
| MongoDB | Read-optimized database |
| PostgreSQL | Write-secure database |
| NGINX | Load balancer software |
| Consul | Service Discovery (HashiCorp) |
| Docker | Containerization |
| Kubernetes / Minikube | Container Orchestration |
| JWT | Stateless Authentication |
| OAuth 2.0 | Delegated authorization |
| passport.js | Node.js authentication (GitHub strategy) |
| accesscontrol | RBAC (Role-Based Access Control) |
| express-rate-limit | Rate limiting (DDoS protection) |
| express-validator | Validation of inputs (injection protection) |
| helmet | Secure HTTP headers |
| Artillery | Load testing |
| Let’s Encrypt | Automated SSL/TLS certificates |
| OpenSSL | Certificate 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