From Monolith to Microservices: A Student’s Journey with Node.js

For my final software architecture course, we had to build a small e-commerce application. Like most students, I started by building a “monolith.” All my code—user authentication, product catalog, and order processing—was in a single Node.js Express application, connected to a single database. It worked, and it was simple to get started.

But then, the requirements started getting more complex. A change to the order processing logic could accidentally break the user login. The database schema became a tangled mess. Deploying a small fix required re-deploying the entire application. Our professor then introduced us to the concept of microservices, and we decided to refactor our monolith. It was a challenging but incredibly valuable experience.

The Big Breakup: Deconstructing the Monolith

The first step was to identify the “bounded contexts” of our application. We broke our single application into three smaller, independent services:

  1. User Service: Responsible for everything related to users: registration, login, profiles. It had its own database with just the users table.
  2. Product Service: Managed the product catalog. It had its own database with the products table.
  3. Order Service: Handled the shopping cart and order processing. It had its own database with the orders table.

Each service was a completely separate Node.js application. This was the core principle of microservices: independent deployment and a database per service.

The New Boss: The API Gateway

With our services separated, we immediately ran into a new problem: how does the frontend know which service to talk to? The solution was to introduce an API Gateway.

The API Gateway is a single entry point for all incoming requests. It acts like a traffic cop, routing requests to the appropriate service. A request to /api/users would go to the User Service, while a request to /api/products would go to the Product Service. This kept our frontend simple and decoupled it from the backend architecture.

How Do Services Talk to Each Other?

This was the trickiest part. What happens when a new order is placed? The Order Service needs to tell the User Service to update the user’s order history.

Our first instinct was to have the Order Service make a direct API call to the User Service. But our professor warned us that this creates tight coupling. If the User Service is down, the Order Service can’t place an order.

The better solution was event-driven communication. We set up a simple event bus (using a library like RabbitMQ, though for our simple project, Node’s EventEmitter was enough to learn the concept). When the Order Service placed an order, it didn’t call the User Service directly. Instead, it published an order.placed event. Other services, like the User Service or a new Notification Service, could then subscribe to this event and react accordingly. This decoupled our services completely. They didn’t need to know about each other; they only needed to know about the events.

Staying Resilient: The Circuit Breaker

What if our Product Service needed to call an external API to get shipping rates, and that external API was slow or down? We didn’t want our entire Product Service to fail because of a problem with a third-party service.

This is where we learned about the Circuit Breaker pattern. A circuit breaker wraps our external API calls. If it detects too many failures, it “opens the circuit” and immediately fails any new requests without even trying to call the slow API. After a timeout, it enters a “half-open” state to see if the external API has recovered. This prevented a single point of failure from cascading through our entire system.

The Lesson: Complexity is a Trade-off

Moving from a monolith to microservices was a huge learning experience. It introduced a lot of new complexity: we had to manage multiple databases, set up an API Gateway, and think about inter-service communication. But the benefits were clear. Our services were smaller and easier to understand. We could deploy a fix to the Product Service without touching the User Service. And it forced us to think about resilience and fault tolerance in a way we never had to with our monolith.

For a student, it’s a powerful lesson in software design. It’s not about which architecture is “best,” but about understanding the trade-offs and choosing the right tool for the job.

0%