Microservice Example

Splitting the Monolith: A Journey in Refactoring

In my prior musically themed musing I discussed the major themes and perspective on how leveraging microservices enhance an application. Now I want to provide a spefific example by imagining a monolithic Node.js e-commerce application that has grown significantly over time. This application manages products, processes orders, and handles user accounts. All of these components are intertwined within a single codebase, relying on the same database, and hosted on a single server.

Step 1: Identify Distinct Components
Before any code is written, one must analyze the monolithic application and determine its distinct functionalities. In our e-commerce example, we can identify three primary components:

  1. Product Management
  2. Order Processing
  3. User Management

Step 2: Extracting the Components
To begin the transformation, each component will be extracted into its own separate service. This involves:

  • Creating separate repositories for each service.
  • Isolating the logic, routes, and data models relevant to each component.

For instance, for Product Management, you’d extract all routes, logic, and data models that relate to products (listing, adding, editing, deleting products).

Step 3: Decouple the Database
Each service should ideally have its own dedicated database. This decoupling ensures that services are not tightly intertwined at the data level.

For the User Management service, this would involve extracting all user-related tables (like users, user_roles, etc.) into a separate database. This database would then be exclusively accessed by the User Management service.

Step 4: Create Communication Mechanisms
In a microservice architecture, services often need to communicate. Instead of direct function calls like in a monolithic system, services might communicate via APIs or message queues.

Imagine an order is placed. The Order Processing service might need product information. It would send a request to the Product Management service API, which in turn would send back the relevant data.

Step 5: Deploying the Services
With our services separated, each can now be deployed individually. This could be on separate servers, different containers, or even across various cloud providers. The benefit? If there’s heavy traffic affecting product listings, the Product Management service can be scaled without affecting Order Processing or User Management.


In Practice: JavaScript & Express.js Example

Consider the monolithic Express.js route handling products:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const express = require('express');
const app = express();

// ... (other routes and middleware)

app.get('/products', (req, res) => {
// fetch and return products
});

app.get('/orders', (req, res) => {
// fetch and return orders
});

app.get('/users', (req, res) => {
// fetch and return users
});

app.listen(3000);

When refactored into microservices, each service might look like:

Product Service

1
2
3
4
5
6
7
8
const express = require('express');
const app = express();

app.get('/products', (req, res) => {
// fetch and return products
});

app.listen(3001);

Order Service

1
2
3
4
5
6
7
// ... 

app.get('/orders', (req, res) => {
// fetch and return orders
});

app.listen(3002);

User Service

1
2
3
4
5
6
7
// ...

app.get('/users', (req, res) => {
// fetch and return users
});

app.listen(3003);

With this separation, development, deployment, and scaling become more granular and manageable. Each service, living in its own universe, harmoniously interacts with others, making the application more resilient and adaptable.

Embracing the microservices journey, especially in JavaScript’s dynamic ecosystem, promises flexibility and scalability. While the upfront refactoring might seem challenging, the long-term benefits often outweigh the initial costs.