DEV Community

Raihanul Islam Sharif
Raihanul Islam Sharif

Posted on

How I Built a Microservices System from Scratch (No Tutorial)

How I Built a Microservices System from Scratch (No Tutorial)

Most microservices tutorials start with a perfect architecture diagram and end with a clean GitHub repo you can clone. Mine started with me staring at a blank screen wondering "where do I even begin?"

This is the real story of how I built my HR Management System — a production-deployed microservices project with an API Gateway, Auth Service, Employee Service, and Attendance Service — containerized with Docker and live on Render.com.

No hand-holding. No pre-built boilerplate. Just decisions, mistakes, and lessons.


Why Microservices? (The Honest Answer)

Let me be upfront: for a portfolio project, a monolith would have been faster to build.

But I wasn't building for speed. I was building to understand how real systems work at scale — the kind of systems I want to work on professionally. If I was going to spend weeks on a project, I wanted to come out the other side actually knowing something.

So I committed to microservices. And then immediately had to figure out what that actually meant in practice.


The Architecture (What I Landed On)

After a lot of reading and a few failed attempts, I settled on four core services:

Client
  │
  ▼
API Gateway  ←── single entry point for all requests
  │
  ├── Auth Service       (JWT, login, register)
  ├── Employee Service   (CRUD, employee data)
  └── Attendance Service (check-in/out, records)
Enter fullscreen mode Exit fullscreen mode

Each service:

  • Has its own Express.js server
  • Connects to its own MongoDB collection (logically separated)
  • Runs in its own Docker container
  • Communicates through the API Gateway only

The client never talks to individual services directly. Everything goes through the gateway. This was the single most important architectural decision I made.


The First Real Problem: How Do Services Talk to Each Other?

This sounds obvious until you actually try to do it.

In a monolith, you just call a function. In microservices, you're making HTTP requests between containers — and suddenly you have to think about:

  • What if a service is down?
  • How does the Gateway know where each service lives?
  • How do I avoid hardcoding localhost:3001 everywhere?

My solution was straightforward: environment variables for service URLs, and Docker Compose to wire everything together locally.

# docker-compose.yml (simplified)
services:
  api-gateway:
    build: ./api-gateway
    ports:
      - "3000:3000"
    environment:
      - AUTH_SERVICE_URL=http://auth-service:3001
      - EMPLOYEE_SERVICE_URL=http://employee-service:3002
      - ATTENDANCE_SERVICE_URL=http://attendance-service:3003

  auth-service:
    build: ./auth-service
    ports:
      - "3001:3001"
    environment:
      - MONGODB_URI=${MONGODB_URI}
      - JWT_SECRET=${JWT_SECRET}
Enter fullscreen mode Exit fullscreen mode

Inside Docker Compose, services can reference each other by service name — not localhost. This tripped me up for an embarrassing amount of time before I understood Docker's internal networking.


JWT Auth Across Services — The Tricky Part

Authentication in a monolith: middleware checks the token, done.

Authentication in microservices: every service needs to verify the token, but you don't want each service depending on the Auth Service for every request. That creates tight coupling and a single point of failure.

My approach: verify the JWT at the Gateway level.

// api-gateway/middleware/auth.js
const jwt = require('jsonwebtoken');

const verifyToken = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];

  if (!token) {
    return res.status(401).json({ message: 'No token provided' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.headers['x-user-id'] = decoded.id;
    req.headers['x-user-role'] = decoded.role;
    next();
  } catch (err) {
    return res.status(401).json({ message: 'Invalid token' });
  }
};
Enter fullscreen mode Exit fullscreen mode

The Gateway verifies the token and forwards the user's ID and role as custom headers to downstream services. Those services trust those headers — they don't re-verify the JWT. Clean, decoupled, and fast.


Dockerizing Everything

Each service has its own Dockerfile:

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3001
CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

Then I pushed each image to Docker Hub under my account (raihanuldev) so they could be pulled and deployed independently.

docker build -t raihanuldev/auth-service:latest .
docker push raihanuldev/auth-service:latest
Enter fullscreen mode Exit fullscreen mode

This taught me something important: the build process and the run process are completely separate. You build once, run anywhere. That separation changes how you think about deployment.


Deploying to Render.com

Render.com let me deploy each Docker container as a separate web service — which is exactly how microservices should be deployed.

The catch: Render's free tier spins down services after inactivity. When all four services spin down simultaneously and a request comes in, the cold start chain can take a while.

For production, you'd solve this with health check pings or a paid tier. For a portfolio project, I added a note in the README explaining it. Honesty > pretending it's perfect.

Each service gets its own Render URL, and the API Gateway's environment variables point to those URLs. Swap localhost for production URLs, and the same code runs everywhere.


What I'd Do Differently

1. Start with the data model first.
I jumped into building services before fully thinking through how data would flow between them. Employee IDs referenced in the Attendance Service caused me headaches early on.

2. Add a shared error format from day one.
Each service was returning errors in slightly different formats. Standardize this early — your frontend (and your sanity) will thank you.

3. Logging matters more than you think.
When something breaks in a distributed system, you need logs from multiple services to understand what happened. I added proper logging late. Should have been the first thing.


What I Actually Learned

Building this forced me to understand things that tutorials skim over:

  • Docker networking — how containers actually talk to each other
  • Stateless services — why JWT is a natural fit for microservices
  • Deployment complexity — why DevOps is a real job
  • Trade-offs — microservices aren't "better," they're a trade-off between complexity and scalability

The most valuable outcome wasn't the project itself. It was being able to look at a system architecture diagram and understand the decisions behind it — not just copy them.


The Code

The project is live on GitHub: github.com/raihanuldev

Docker images are on Docker Hub: raihanuldev/auth-service, raihanuldev/employee-service, etc.


If you're thinking about building a microservices project — don't wait until you feel "ready." You learn the hard parts by hitting them. Start small, pick 2–3 services, and wire them together. The architecture diagram makes sense after you've built it, not before.


I'm a Full Stack Engineer (MERN + ASP.NET) based in Bangladesh, currently open to remote internships and junior engineer roles. If you're working on interesting systems, let's connect.


Tags: #microservices #node #docker #webdev

Top comments (0)