From Monolith to Microservices: A Student's Journey with Node.js
From Monolith to Microservices
When I started building my final project for software architecture class — a small e-commerce platform — I wrote everything in a single Node.js app.
User authentication, product catalog, order processing — all in one Express project with a single PostgreSQL database.
It worked beautifully.
Until it didn’t.
As features grew, one innocent change in the order logic broke user registration. The database schema looked like a spider web. Deploying even a small fix meant redeploying everything.
That’s when our professor introduced the phrase that every backend engineer eventually meets:
“bounded context.”
And with it came the idea of microservices — the architecture that promised freedom… and delivered complexity.
1. The Big Breakup: Deconstructing the Monolith
We began by asking a deceptively simple question: Where does one concern end and another begin?
After a few whiteboard debates, we identified three distinct “bounded contexts” — or business domains — in our app:
- User Service — registration, login, profiles.
Database:userstable only. - Product Service — catalog management and inventory.
Database:productstable. - Order Service — shopping cart and checkout.
Database:orderstable.
Each became an independent Node.js microservice, running on its own port, with its own database.
That decision — one database per service — felt radical at first.
But it enforced a powerful rule: no service could silently reach into another’s data. Every interaction had to happen through well-defined APIs or messages.
This was my first real exposure to data ownership — the idea that architecture isn’t just about code separation, but about autonomy and accountability.
2. The New Middleman: Building an API Gateway
Once we split everything apart, the next question emerged:
How does the frontend talk to all these different services?
The answer was an API Gateway.
The gateway became our single entry point — a traffic controller that routed /api/users to the User Service, /api/products to the Product Service, and so on.
1 | app.use('/api/users', proxy('http://localhost:3001')); |
It didn’t just simplify routing — it became a policy layer.
We implemented JWT authentication, rate limiting, and logging here, which meant every request passed through a consistent security and monitoring boundary.
It was my first glimpse into how “infrastructure concerns” live at a different layer than “business logic.”
That separation made the system feel engineered instead of just coded.
3. The Hard Part: Making Services Talk to Each Other
The real challenge wasn’t splitting the app — it was reconnecting the pieces.
When an order was placed, the Order Service needed to notify the User Service (“update order history”) and sometimes the Product Service (“decrease inventory”).
Our first naive approach? Direct HTTP calls:
1 | await axios.post('http://users-service:3001/api/updateHistory', {...}); |
It worked — until it didn’t.
If the User Service went down, the entire checkout process failed.
Our “independent” services were, in fact, tightly coupled.
Our professor then introduced us to event-driven architecture.
We implemented a simple event bus using Node’s built-in EventEmitter (and later RabbitMQ).
When an order was created, the Order Service emitted an event:
1 | eventBus.emit('order.placed', order); |
Other services subscribed and reacted independently:
1 | eventBus.on('order.placed', handleUserUpdate); |
It felt liberating.
For the first time, services didn’t know about each other — they just listened for events that mattered to them.
That’s when I understood the deeper lesson:
Microservices aren’t about splitting code. They’re about decoupling communication.
4. Resilience: Learning the Hard Way
One of our microservices — the Product Service — depended on a third-party shipping API for rate calculations.
When that API went down, our whole checkout process froze.
That’s when we discovered the Circuit Breaker pattern.
A circuit breaker wraps external API calls, watching for repeated failures. If an endpoint keeps failing, it “opens the circuit” and short-circuits future calls immediately.
Here’s a simplified example:
1 | if (failures >= threshold) { |
It sounds small, but it’s profound.
It teaches you to design for failure, not against it.
Adding circuit breakers, retries, and timeouts transformed our brittle experiment into something that could fail gracefully.
For the first time, our system behaved less like a collection of scripts — and more like a distributed application.
5. The Trade-Off: Freedom vs. Complexity
After months of iteration, our microservices worked — independently deployable, fault-tolerant, and event-driven.
But the victory was bittersweet.
We had gained:
- Clearer ownership and smaller codebases.
- Independent deployment pipelines.
- A real appreciation for asynchronous design.
But we had paid with:
- Complex local development setups (Docker Compose became mandatory).
- Multiple databases to maintain.
- New debugging challenges — tracing one request across three logs.
At one point, I realized that my “hello world” requests now passed through five processes and two queues before returning a response.
It was overkill for a small app, but a priceless lesson in trade-offs.
Architecture isn’t about chasing elegance.
It’s about finding the right amount of complexity for your scale and context.
6. My Takeaway: The Mindset Shift
Moving from a monolith to microservices changed more than my code — it changed how I think about systems.
In a monolith, I thought in functions.
In microservices, I had to think in boundaries, contracts, and failure modes.
It’s not that microservices are “better.”
They’re just different tools for different problems.
For a student, that realization is transformative:
Scalability isn’t about handling more users — it’s about handling more complexity.
Microservices taught me that good architecture isn’t the one with the most services or the fanciest diagrams.
It’s the one that you can reason about, debug under pressure, and evolve without fear.
And that, for me, was the real graduation.
- Title: From Monolith to Microservices: A Student's Journey with Node.js
- Author: Zhaokun Wang
- Created at : 2025-03-25 15:30:00
- Updated at : 2025-03-30 00:00:00
- Link: https://iamzhaokun.com/2025/03/25/nodejs-microservices/
- License: This work is licensed under CC BY-NC-SA 4.0.